Quickstart
Add a human approval to an Inngest function in a few minutes. By the end you'll have a function that pauses for a decision waiting in the Inbox — with no change to how you run or deploy your workflows.
1. Install the SDK + Inngest adapter
This quickstart runs Flowplane on top of Inngest — the default path. You keep your own Inngest functions; Flowplane is one step inside them.
npm install @flowplane/sdk @flowplane/adapter-inngest inngest2. Create the Flowplane client
The Flowplane client holds your API key. Note it does not take your Inngest client — Flowplane never wraps or owns your function.
const inngest = new Inngest({ id: 'acme-expenses' }) // No `inngest` client needed here — Flowplane is just a step.const flowplane = new FlowplaneInngest({ flowplaneApiKey: process.env.FLOWPLANE_API_KEY!, flowplaneApiUrl: process.env.FLOWPLANE_API_URL ?? 'https://flowplane-api.do.demo.thebuildmill.com',})
3. Add an approval step to your function
Inside your own inngest.createFunction, call flowplane.awaitApproval(ctx, …) wherever a human should decide. It returns once a reviewer decides — or auto-resolves if the policy gate says no human is needed.
export const expenseApproval = inngest.createFunction( { id: 'expense-approval' }, { event: 'acme/expense.submitted' }, async (ctx) => { const { event, step } = ctx const amount = (event.data as { amount: number }).amount // Pause this run for a human decision. Inngest pauses here (via // step.waitForEvent) until Flowplane records the decision and resumes the // run. Your function keeps full control — this is one step, not a wrapper. const decision = await flowplane.awaitApproval(ctx, { id: 'manager-review', required: amount > 1000, policy: 'expenses', assigneeEmail: 'manager@acme.com', prompt: `Approve a $${amount.toFixed(2)} expense?`, context: { aiReasoning: 'Amount is within the team budget; vendor is known.', blocks: [ { kind: 'fields', title: 'Expense', fields: [{ label: 'Amount', value: `$${amount.toFixed(2)}` }], }, ], }, }) if (!decision.approved) { return { status: 'rejected', by: decision.decidedBy } } return await step.run('disburse', () => ({ status: 'paid', amount })) },)
policy and required drive the gate (see Human-in-the-loop): the caller proposes required, and the named policy's floor can force approval. context.blocks is what the reviewer sees. The id names this approval as a step within your function (it must be unique and stable across replays).
4. Serve it like any Inngest function
Nothing special here — expenseApproval is an ordinary Inngest function:
import { serve } from 'inngest/next'export default serve({ client: inngest, functions: [expenseApproval] })
5. Decide
Send the triggering event (amount > 1000 to force a human). The run parks at awaitApproval, and the decision shows up in the Inbox with its context. Approve or reject there — the Inngest run resumes on the branch you chose.
Flowplane is a step, not an orchestrator
awaitApproval slots into the function you already own and run. Flowplane only ever sees the decision metadata — never your code, your other steps, or your credentials. The same call works on the bundled SDK worker and future Temporal / Trigger.dev adapters.
Next: the full Human-in-the-loop guide — context, policy gates, quorum, and SLAs.