Tawa docs
Guides

The review flag

Scans return ALLOW or BLOCK. A needsReview flag marks the uncertain ones, for tuning and for routing to a second model.

A scan result carries one decision to act on, action, which is allow or block. When the engine was uncertain about that decision, the result also carries needsReview: true. The flag does not hold the request: the action is already final, and the flag marks it as one worth a second look.

FieldTypeMeaning
action"allow" | "block"What to enforce. Already collapsed by the gateway
engineActionScanActionThe engine's own label before collapsing: allow, review, block, hard_block
needsReviewbooleantrue when the engine's action was review. ood does not set it
const result = await tawa.analyze(userInput);

result.action;       // "block"
result.engineAction; // "review"
result.needsReview;  // true

How a review becomes allow or block

The engine can answer ALLOW, REVIEW, BLOCK, or HARD_BLOCK. The gateway turns that into one of two actions before the response is sent, using a per-app setting for each content type.

Engine actionaction you receive
ALLOW, OODallow
BLOCK, HARD_BLOCK, SANDBOXblock (never collapsed to allow)
REVIEW, SOFT_BLOCK, AMBIGUOUSallow or block, per the setting below

Shadow mode still forces allow, and a boundary rule can still force block, after the collapse. Neither changes engineAction or needsReview.

The reviewCollapse setting

Each app has one allow or block value per content type. It decides what a REVIEW becomes for that type.

Content typeScan methodsDefault
user_promptanalyzeblock
tool_callscanToolCallblock
tool_outputscanToolOutputallow
rag_chunkscanChunk, each chunk of scanRetrievalallow
rag_contextscanContextallow
llm_responsescanResponseallow

An absent or unrecognised value uses the default for that type. A /scan request that sets provenance uses the setting for that provenance. Knowledge base scans are not affected.

Set it per app in the dashboard, on the app's Settings page, in the card titled "Results that need review". The card has one Block or Allow choice for each content type above, labeled User prompts, Tool calls, Tool results, Retrieved chunks, Assembled context and Model responses, and the flag stays visible either way.

Default behavior change

With the defaults, a user prompt or tool call the engine labels REVIEW is returned as block. Previously the result carried review, which the SDK's default shouldBlock did not block. To keep the previous pass-through for an app, set that content type to allow.

Choosing a setting

block converts every REVIEW into a block, which catches more attacks and also blocks some benign input. allow lets every REVIEW through, which blocks less and misses more. The figures below were measured on our own evaluation sets, with 95% Wilson confidence intervals. They show the effect of the setting on those sets, not a prediction for your traffic.

Content (benign / malicious rows)SettingBenign blockedMalicious blocked
Prompts, benchmark set (432 / 984)allow0.0% [0.0, 0.9]25.4% [22.8, 28.2]
block5.1% [3.4, 7.6]81.5% [79.0, 83.8]
Prompts, benign injection-like set (102 / none)allow0.0% [0.0, 3.6]n/a
block21.6% [14.7, 30.5]n/a
Tool outputs, synthetic JSON (100 / 120)allow0.0% [0.0, 3.7]25.8% [18.8, 34.3]
block5.0% [2.2, 11.2]90.8% [84.3, 94.8]
Tool calls, synthetic JSON (60 / 60)allow0.0% [0.0, 6.0]6.7% [2.6, 15.9]
block3.3% [0.9, 11.4]66.7% [54.1, 77.3]
Retrieved chunks, real documents (1374 / none)allow4.9% [3.9, 6.1]n/a
block50.4% [47.8, 53.1]n/a
Assembled context, real documents (280 / none)allow22.5% [18.0, 27.7]n/a
block81.4% [76.5, 85.5]n/a

What the measurements support:

  • For prompts, block raises recall substantially and costs false positives, most visibly on benign prompts that resemble injections.
  • For retrieved documents, block would block roughly half or more of the real benign documents measured, which is why rag_chunk and rag_context default to allow.
  • On real benign documents the flag itself is frequent (45.6% [42.9, 48.2] of the retrieved chunks measured), so for retrieved content it is a coarse review queue.
  • The tool call and tool output sets are small and synthetic. There is no labelled data for llm_response.

Using the flag

Tune thresholds with it

needsReview marks the decisions nearest the engine's cut points, so it is the natural place to look when deciding whether a setting is too strict or too loose.

  • Filter the dashboard's Jobs list by Verdict, choosing Needs review, and read a sample. Flagged items that were blocked and are fine argue for allow on that content type. Flagged items that were allowed and are not fine argue for block.
  • Record those judgments with the job feedback controls.
  • Count flagged scans per period from the review and needsReview counts of the Analytics API, or per content type in your own logs.

Route flagged items to a second model

A flagged result is a cheap trigger for a second opinion. The pattern is the same in every integration: act on action as usual, and call a second reviewer only when needsReview is true. secondOpinion below is your own function, backed by whatever model or service you choose.

declare function secondOpinion(input: {
  text: string;
  stage: string;
  risk: number | null;
  rationale: string;
}): Promise<"safe" | "unsafe">;

const result = await tawa.analyze(userInput);

if (result.needsReview) {
  const verdict = await secondOpinion({
    text: userInput,
    stage: "prompt",
    risk: result.risk,
    rationale: result.rationale,
  });
  if (verdict === "unsafe") throw new Error("Blocked after review");
  // verdict === "safe": proceed even if result.action was "block"
} else if (result.action === "block") {
  throw new Error("Blocked");
}

With the Vercel AI SDK middleware, pass onReview. It is called once per scan whenever needsReview is true, whether or not the scan blocked, and before the block decision. It receives (result, stage, text): the ScanResult, "prompt" or "response", and the scanned text. It may return a promise, which the middleware awaits. An error thrown inside it does not affect the scan: it goes to the optional onError callback, or is dropped when onError is not set.

const model = tawa(openai("gpt-5"), tawaClient, {
  scanOptions: { sessionId },
  onReview: async (result, stage, text) => {
    await reviewQueue.add({ stage, text, risk: result.risk, rationale: result.rationale });
  },
  onError: (error) => console.error("onReview failed", error),
});

onReview observes. It does not change whether the call is blocked; to override a block after a second opinion, use the raw SDK pattern above.

Watch flags on the live stream

Decision events carry needsReview and enforcedAction. action on an event stays the engine's label, so a flagged tool call that was blocked reads as follows.

const stream = tawa.subscribe(sessionId);
stream.on("tawa:event", (event) => {
  if (event.stage === "audit" || event.stage === "_meta") return;
  if (event.needsReview) {
    console.log(event.stage, event.action, "->", event.enforcedAction);
  }
});
// tool review -> block

On the wire the same fields are needs_review and enforced_action; see the live event stream.

Retrieval scans

scanRetrieval applies the setting to each chunk. aggregate.worstAction and aggregate.flaggedChunkIds are computed from the collapsed actions. aggregate.needsReview and aggregate.reviewChunkIds list the chunks the engine flagged, including the ones enforced as allow.

const { aggregate } = await tawa.scanRetrieval(query, chunks);

aggregate.flaggedChunkIds; // enforced as block
aggregate.reviewChunkIds;  // flagged for review, blocked or not

What is stored

WhereValue
Scan response decision.actionALLOW or BLOCK after the collapse
Scan response decision.engine_actionThe engine's label
Persisted job (result_action, dashboard, Analytics API, exports)The engine's label, so a flagged job reads REVIEW
Live event actionThe engine's label

The Analytics API counts persisted jobs, so its review count (also returned as needsReview) includes flagged scans that you received as allow or block.

Older SDKs and gateways

The new fields are additive.

  • An SDK without needsReview ignores the fields it does not know. Its action is still a valid value (allow or block).
  • A new SDK against a gateway that does not send the fields derives needsReview from action === "review" and engineAction from action. Against such a gateway action is the engine's raw label, so review can still appear.
  • Do not require the fields to be present. Treat a missing needsReview as false in clients you maintain yourself.

On this page