Skip to content

approvalGate

Insert a conditional or always-on human approval / review gate into a flow. On the first run the gate pauses execution, stores a prompt on shared state, and throws an InterruptError. When the human responds the caller resumes the flow with the response injected into shared state, and the gate processes the outcome — setting approved, humanEdit, or humanFeedback accordingly.

Import

typescript
import { approvalGate } from "flowneer/presets/pipeline";

Usage

typescript
import { FlowBuilder, InterruptError } from "flowneer";
import { resumeFlow } from "flowneer/plugins/agent";
import { approvalGate } from "flowneer/presets/pipeline";

interface DraftState {
  draft: string;
  approved?: boolean;
  humanEdit?: string;
  humanFeedback?: string;
  __humanPrompt?: string;
}

const flow = new FlowBuilder<DraftState>()
  .startWith(async (s) => {
    s.draft = await generateDraft(s.topic);
  })
  .add(
    approvalGate({
      prompt: (s) => `Please review the following draft:\n\n${s.draft}`,
      onReject: (s, feedback) => {
        console.log("Rejected:", feedback);
        // throw to stop, or set state to trigger a revision anchor
      },
    }),
  )
  .then((s) => {
    if (!s.approved) return;
    const content = s.humanEdit ?? s.draft;
    console.log("Publishing:", content);
  });

// ─── First run ────────────────────────────────────────────────────

try {
  await flow.run(state);
} catch (e) {
  if (e instanceof InterruptError) {
    // Deliver the prompt to a human (email, Slack, UI, etc.)
    console.log("Awaiting review:", e.savedShared.__humanPrompt);

    // Later, when the human responds:
    await resumeFlow(flow, e.savedShared, {
      __approvalResponse: "approve", // or "edit: revised text", or "reject"
    });
  }
}

Options

OptionTypeDefaultDescription
promptstring | (s, p) => string | Promise<string>Approve this output?\n\n<JSON of s.output>Prompt message stored on shared.__humanPrompt before interrupting
condition(s, p) => boolean | Promise<boolean>Always interruptWhen provided, the gate only activates when this returns true
onReject(s, feedback?) => void | Promise<void>() => { throw new Error("Rejected by human") }Called when the response is not "approve", "yes", or "edit: …". Throw to halt, or mutate to redirect
responseKeystring"__approvalResponse"Key on shared where the human's response is injected during resumeFlow

State keys

Internal (cleaned up after resume)

KeyDescription
__humanPromptThe resolved prompt string, set before interrupting
__approvalResponseThe human's response, injected by the caller on resume

Written by the gate on resume

KeyDescriptionCondition
approvedtrue when approved or edited, false when rejectedAlways set on resume
humanEditThe revised text (everything after "edit: ")Only when response starts with "edit: "
humanFeedbackThe raw rejection stringOnly on rejection

Response format

The human's response (value of shared[responseKey]) is parsed as follows:

ResponseOutcome
"approve" or "yes" (any case)approved = true
"edit: <text>"humanEdit = "<text>", approved = true
Anything elseapproved = false, humanFeedback = <text>, onReject() called

Resume pattern

typescript
// After catching InterruptError:
await resumeFlow(flow, e.savedShared, {
  __approvalResponse: "edit: revised version of the content",
});
// → s.approved = true, s.humanEdit = "revised version of the content"

resumeFlow merges the second argument (your partial edits) into the saved state and re-runs the flow from the beginning. The gate step detects __approvalResponse is set, processes it, and falls through to the next step.

Conditional gate

Only activate the gate when confidence is low:

typescript
.add(
  approvalGate({
    condition: (s) => s.confidence < 0.8,
    prompt: (s) => `Low-confidence output (${s.confidence}). Approve?\n\n${s.output}`,
  }),
)

When condition returns false the gate step is a no-op — the flow continues as if it were not there.

Return value

approvalGate() returns a FlowBuilder<S, P> with a single step. Compose it with plugins:

typescript
const gate = approvalGate({ prompt: reviewPrompt })
  .withTiming()
  .withCheckpoint({ save, load, key: (s) => s.jobId });

Or splice it mid-flow via .add():

typescript
const pipeline = new FlowBuilder<State>()
  .startWith(generate)
  .add(approvalGate({ prompt: reviewPrompt }))
  .then(publish);

See Also