Skip to main content
Human steps pause a workflow until a manual decision or additional information is provided. Declare them with createHuman and reuse them across workflows.
import { createHuman, createStep, createWorkflow } from "@ai_kit/core";
import { z } from "zod";

const fetchDraftApplication = createStep<
  { applicantId: string },
  { applicantId: string; amount: number; history: string[] }
>({
  id: "fetch-draft-application",
  description: "Load a case awaiting validation",
  inputSchema: z.object({ applicantId: z.string().min(1) }),
  handler: async ({ input }) => ({
    applicantId: input.applicantId,
    amount: 4200,
    history: ["2019: opened", "2023: updated"],
  }),
});

export const manualReview = createHuman<
  { applicantId: string; amount: number; history: string[] },
  { decision: string; comment: string }
>({
  id: "manual-review",
  output: ({ current }) => ({
    case: {
      id: current.applicantId,
      amount: current.amount,
      history: current.history,
    },
  }),
  input: ({ ask }) =>
    ask.form({
      title: "Manual case validation",
      fields: [
        ask.text({ id: "comment", label: "Comment" }),
        ask.select({
          id: "decision",
          label: "Decision",
          options: ["approve", "reject"],
        }),
      ],
    }),
});

const finalizeDecision = createStep<
  { decision: string; comment: string },
  { status: string }
>({
  id: "finalize-decision",
  handler: ({ input, context }) => ({
    status: `${context.initialInput.applicantId}:${input.decision}:${input.comment}`,
  }),
});

export const onboardingWorkflow = createWorkflow({ id: "onboarding" })
  .then(fetchDraftApplication)
  .human(manualReview)
  .then(finalizeDecision)
  .commit();
  • The output of fetchDraftApplication is injected into manualReview (output.current) and delivered to the client through pendingHuman.output.
  • The human response becomes the input of finalizeDecision, which can also access the original data via context.initialInput.

Execution cycle

const run = onboardingWorkflow.createRun();
const { stream, final } = await run.stream({ inputData: { applicantId: "app-123" } });

for await (const event of stream) {
  if (event.type === "step:human:requested") {
    const { form, output } = event.data;
    ui.displayReviewForm(form, output.case);
  }
}

const pending = await final;

if (pending.status === "waiting_human") {
  await run.resumeWithHumanInput({
    stepId: "manual-review",
    data: { decision: "approve", comment: "Looks good" },
  });
}
run.resumeWithHumanInput validates the response using the human step schema and resumes the workflow until completion.

Best practices

  • Use stable identifiers for human steps—workflow resumes rely on them.
  • Send rich metadata in output to simplify UI rendering (summaries, links, stats).
  • Record step:human:* events in your observability platform to track turnaround times.