{"slug":"tako-workflows-in-nextjs-via-instrumentation","url":"https://tako.sh/blog/tako-workflows-in-nextjs-via-instrumentation/","canonical":"https://tako.sh/blog/tako-workflows-in-nextjs-via-instrumentation/","title":"Next.js instrumentation.ts meets initServerRuntime","date":"2026-04-24T13:48","description":"Drop a five-line instrumentation.ts into your Next.js app and Tako workflows, signals, and channel publishes light up inside routes and server actions — no ambient globals, no bootstrap glue.","author":null,"image":"06d309d2307c","imageAlt":null,"headings":[{"depth":2,"slug":"why-nextjs-needs-a-boot-hook","text":"Why Next.js needs a boot hook"},{"depth":2,"slug":"what-initserverruntime-does","text":"What initServerRuntime() does"},{"depth":2,"slug":"wire-it-up","text":"Wire it up"},{"depth":2,"slug":"the-bigger-picture","text":"The bigger picture"}],"markdown":"Next.js ships a lifecycle hook called [`instrumentation.ts`](https://nextjs.org/docs/app/guides/instrumentation). We just exposed `initServerRuntime()` from `tako.sh/internal`. Snap them together and Tako's durable [workflows](/blog/durable-workflows-are-here), cross-process [signals](/blog/pause-a-workflow-until-a-human-clicks-approve), and [channel publishes](/blog/durable-channels-built-in) start working inside your Next.js routes and server actions. Five lines, one file.\n\n## Why Next.js needs a boot hook\n\nMost Tako apps are a single fetch handler — the SDK's runtime entrypoint imports your module and the workflow/channel plumbing is installed in the same process that handles requests. Next.js standalone is structured differently. Our [Next.js adapter](/docs/framework-guides#nextjs) wraps `next start` and spawns it as a child process, then proxies requests to it. The Tako SDK's boot hook fires in the parent, but your `app/` and `pages/` handlers execute in the child.\n\n```d2\ndirection: right\n\nparent: tako.sh/nextjs wrapper {style.fill: \"#E88783\"; style.font-size: 14}\nchild: next start (your routes) {style.fill: \"#9BC4B6\"; style.font-size: 14}\nruntime: Tako runtime\\n(workflows, channels, signals) {shape: cylinder; style.fill: \"#FFF9F4\"; style.stroke: \"#2F2A44\"; style.font-size: 14}\n\nparent -> child: spawn + proxy\nparent -> runtime: boot hook (only here)\nchild -> runtime: installed by instrumentation.ts\n```\n\nWithout a boot step on the child side, calling `defineWorkflow(...).enqueue(payload)`, `signal(event, payload)`, or `channel.publish(...)` from inside a route throws `TakoError(\"TAKO_UNAVAILABLE\", \"Workflow runtime not installed. ...\")`. Everything else — typed [`secrets`](/blog/secrets-without-env-files), [`env`](/blog/typegen-and-the-ambient-tako-global), `logger`, `tako.gen.ts` imports — already works, because those are static imports that don't depend on process-level state.\n\n## What `initServerRuntime()` does\n\nIt's the same boot step Tako's plain runtime entrypoint performs, now callable directly. One call per process, idempotent, safe to import in any Node context:\n\n| Step                                          | Effect                                                                        |\n| --------------------------------------------- | ----------------------------------------------------------------------------- |\n| Install the channel socket publisher          | `channel.publish(...)` can send to subscribers on other instances             |\n| Register the workflow runtime                 | `defineWorkflow(...).enqueue()` and `signal()` reach the Tako workflow engine |\n| Assert the parent-provided socket env is sane | Fail loud if the child was spawned without Tako's internal env vars           |\n\nIt lives on `tako.sh/internal` because it's plumbing — app code never calls it directly.\n\n## Wire it up\n\nDrop this file at the root of your Next.js project, next to `next.config.ts`:\n\n```ts\n// instrumentation.ts\nexport async function register() {\n  if (process.env.NEXT_RUNTIME === \"nodejs\") {\n    const { initServerRuntime } = await import(\"tako.sh/internal\");\n    initServerRuntime();\n  }\n}\n```\n\nThat's it. Next.js calls `register()` once per server process, before any route runs. The `NEXT_RUNTIME === \"nodejs\"` guard skips the Edge runtime, where `tako.sh/internal` doesn't belong — it reads from a Node fd pipe and speaks over a unix-domain socket.\n\nNow your server code does the obvious thing:\n\n```ts\n// app/api/checkout/route.ts\nimport fulfillOrder from \"@/workflows/fulfill-order\";\nimport orderEvents from \"@/channels/order-events\";\n\nexport async function POST(req: Request) {\n  const order = await req.json();\n  await fulfillOrder.enqueue({ orderId: order.id });\n  await orderEvents({ orderId: order.id }).publish({ type: \"placed\", data: order });\n  return Response.json({ ok: true });\n}\n```\n\nEnqueue a multi-step durable workflow, publish to a live channel, send a `signal()` to a waiting run — all from a standard Next.js route or server action, with typed payloads from the same `defineWorkflow`/`defineChannel` calls you'd use in a plain Tako app.\n\n## The bigger picture\n\nTako's goal for Next.js is for it to feel like any other fetch handler: [`withTako()`](/docs/framework-guides#nextjs) in your config, a generated [`tako.gen.ts`](/blog/typegen-and-the-ambient-tako-global) for typed runtime state and secrets, and now `instrumentation.ts` for the piece the child-process model made awkward. No monkeypatching, no framework globals, no `TakoServer` wrapper object — just Next.js's own lifecycle hook calling one SDK function.\n\nSame backend primitives, same deploy flow, same SDK — [`tako deploy`](/docs/deployment) still rolls a Next.js app exactly like any other Node/Bun app. The [CLI reference](/docs/cli) and [framework guides](/docs/framework-guides) cover the rest."}