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.

bash
npm install @flowplane/sdk @flowplane/adapter-inngest inngest

2. 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.

ts
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.

ts
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:

ts
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.