Runlane
Get started

Add Runlane to your app

Connect tasks, triggers, and workers through one shared runtime.

Use this guide to add Runlane to an existing TypeScript app. The example uses four files so each part has one owner:

src/
├── tasks/send-welcome-email.ts  # task definition and handler
├── runlane.ts                   # lane, queues, and task catalog
├── users/queue-welcome-email.ts # application trigger
└── worker.ts                    # worker process entry point

The example assumes Node.js 22.18.0 or newer, the packages from the local quickstart, and an email HTTP endpoint configured through EMAIL_API_URL and EMAIL_API_TOKEN.

Define the task beside its domain code

Create src/tasks/send-welcome-email.ts. Export the task directly so the runtime can register it:

src/tasks/send-welcome-email.ts
import { task } from '@runlane/core'
import * as z from 'zod'

const env = z
  .object({
    EMAIL_API_TOKEN: z.string().min(1),
    EMAIL_API_URL: z.url(),
  })
  .parse(process.env)

export const sendWelcomeEmail = task({
  id: 'email.send-welcome',
  schema: z.object({ email: z.email() }),
  async run({ email }, context) {
    const response = await fetch(env.EMAIL_API_URL, {
      body: JSON.stringify({ template: 'welcome', to: email }),
      headers: { authorization: `Bearer ${env.EMAIL_API_TOKEN}`, 'content-type': 'application/json' },
      method: 'POST',
      signal: context.signal,
    })
    if (!response.ok) throw new Error(`Email API returned ${response.status}`)
  },
})

The handler validates configuration, calls the email API, passes context.signal, and turns a non-success response into a visible task failure.

The task schema validates data before it enters durable execution.

Create one shared runtime

Create src/runlane.ts. This is the shared runtime module:

src/runlane.ts
import { createRunlane, queue } from '@runlane/core'
import { createLocalLane } from '@runlane/lane-local'

import { sendWelcomeEmail } from './tasks/send-welcome-email.js'

const defaultQueue = queue({ name: 'default', default: true })

export const runlane = createRunlane({
  lane: createLocalLane(),
  queues: [defaultQueue],
  tasks: { sendWelcomeEmail },
})

The exported runlane instance registers the lane, queues, and task catalog in one place.

Import this instance wherever the process needs to trigger work or start a worker. Do not construct a runtime inside a request handler. With the local lane, the trigger path and worker must use the same runtime instance because its storage is process-local.

Trigger from application code

Create src/users/queue-welcome-email.ts beside the application code that decides when a welcome email is needed:

src/users/queue-welcome-email.ts
import { runlane } from '../runlane.js'

export async function queueWelcomeEmail(user: { readonly email: string }) {
  const { run } = await runlane.trigger(runlane.tasks.sendWelcomeEmail, {
    email: user.email,
  })

  return run.id
}

Use the task handle exposed by the runtime catalog. You do not need to pass the runtime through another wrapper.

Store or return run.id when the caller needs to inspect the work later. Triggering accepts the work; it does not wait for the task handler to finish.

Start a worker from the process entry point

Create src/worker.ts and run it as a process entry point:

src/worker.ts
import { runlane } from './runlane.js'

const worker = await runlane.createDeliveryWorker()
const close = () => void worker.close()
process.once('SIGTERM', close)

try {
  await worker.waitUntilClosed()
} finally {
  process.off('SIGTERM', close)
  await worker.close()
  await runlane.close()
}

createDeliveryWorker() starts the runtime. With the local lane, the producer and worker must run in this same process because the lane stores its state in memory.

The SIGTERM hook stops acquisition before closing lane resources. Connect the same sequence to your framework's shutdown hook if it owns the process lifecycle.

Check the integration

Create a test that triggers the registered task, calls await runlane.drain(), and checks the email request or stored output. This proves that the task catalog, trigger path, and worker share the same configuration.

Before splitting producers and workers into separate processes, choose a durable lane. Separate processes cannot share the local lane's memory.

Use the application API reference to look up task and runtime options, return values, defaults, and error handling.

On this page