Set up the workflow engine
Before any workflow, WorkBot needs the engine and a route the engine can call. Your app serves the functions and the engine runs them, as Inngest's dev server on your machine or as Inngest Cloud in production. With the engine in place, a crash, a deploy or a day waiting for a person no longer sends the pipeline back to the top. It resumes at the step that was interrupted.
Study first
- Durable execution. The engine re-runs a function from the top after every pause or failure, and the steps it has already recorded return their saved result instead of running again. A step's result must be JSON. A step's side effects can run twice when its result was not recorded, so make each write that must not repeat a step of its own.
- Inngest's pieces. Your app creates an
Inngestinstance and defines events, each with a name and data. A function has triggers, an event or a cron schedule, and inside itstep.rundoes work,step.waitForEventpauses for an event, andstep.sleepwaits for a time. The engine calls the app's serve route to run each step. - Local development. The dev server,
inngest-cli dev -u <app>/api/inngest, is the engine on your machine, with a dashboard of runs atlocalhost:8288.
Build
- Run
pnpm add inngestandpnpm add -D inngest-cli @inngest/test. Then listinngest-cliunderonlyBuiltDependencies, one of pnpm's settings, inpnpm-workspace.yaml. Its install script fetches its binary, and pnpm runs a package's install script only when the package is listed there. - Create
src/inngest/client.tswithnew Inngest({ id: "workbot", eventKey, signingKey, isDev: !signingKey }), and read the keys fromconfig. Inngest's own switch isINNGEST_DEV=1orisDev: true, but in this build no signing key means dev mode, against the dev server. - Create
src/inngest/events.tswith 3 events. DefineeventType("document/received", { schema })with a Zod schema,document/reviewedwith the decision and the corrected fields, andreminders/requested. - Create
src/app/api/inngest/route.tswithexport const { GET, POST, PUT } = serve({ client, functions }). Inroute.test.ts, callGET(new NextRequest(url), { params: Promise.resolve({}) })and expect status 200 andfunction_count: 3in the JSON. This response is how the engine learns which functions the app serves. The 3 are the workflow, its failure handler and the reminders, which you write in the next 2 parts. - Add
pnpm inngest, which starts the dev server pointed at the app, and addINNGEST_EVENT_KEYandINNGEST_SIGNING_KEYto.env.examplefor Inngest Cloud.
Done when
pnpm test src/app/api/inngestpasses, once the 3 functions exist.- With
pnpm devandpnpm inngestrunning,localhost:8288/functionslistsdocument-approval, its failure handler andreminders.
Technical notes
- The serve route is the only route this repository does not write itself. It is the SDK's handler, listed as the exception in
src/app/api/CLAUDE.md. - The code decides dev mode from the absence of a signing key, not from
INNGEST_DEVin the environment. Soconfig.tsstays the one module that readsprocess.env, and the tests run in dev mode without setup. - On Vercel, the same route serves Inngest Cloud. Install Inngest's Vercel integration, which sets
INNGEST_EVENT_KEYandINNGEST_SIGNING_KEYin the project and syncs the functions on each deploy. - When a run uses up its retries, the dashboard marks it failed and the engine raises an
inngest/function.failedevent.onFailureon the function handles that event, and an alerting function can trigger on it too. So there is no separate dead-letter queue, a holding place for failed messages, to drain.

