Runlane
Build with RunlaneBuild tasks

Define and trigger tasks

Define a typed task, register it, trigger a run, and read the result.

Use this guide when your app needs to start background work without waiting for the task handler to finish. The task, runtime, trigger, and local inspection code live in separate modules.

Define the task and its data

Create src/tasks/generate-invoice.ts:

src/tasks/generate-invoice.ts
import { task } from '@runlane/core'
import * as z from 'zod'

const env = z.object({ INVOICE_API_URL: z.url() }).parse(process.env)
const invoiceSchema = z.object({ id: z.string() })

export const generateInvoice = task({
  id: 'invoice.generate',
  output: z.object({ invoiceId: z.string() }),
  schema: z.object({ accountId: z.string().min(1) }),
  async run({ accountId }, context) {
    const response = await fetch(env.INVOICE_API_URL, {
      body: JSON.stringify({ accountId }),
      headers: { 'content-type': 'application/json' },
      method: 'POST',
      signal: context.signal,
    })
    if (!response.ok) throw new Error(`Invoice API returned ${response.status}`)

    const invoice = invoiceSchema.parse(await response.json())
    return { invoiceId: invoice.id }
  },
})

A task combines a stable id, a Standard Schema-compatible payload schema, and a handler. This task calls the invoice API inside run().

Runlane validates and transforms the payload before creation, then stores the schema output. Every attempt uses that run's saved payload without applying the input schema again. A rejected trigger leaves no partial run.

Original inputs are not retained separately. Keep handlers compatible with saved payloads when deploying a new task definition; changing an input schema does not rewrite existing runs.

When output is present, TypeScript checks the handler return. Runlane validates it before marking the run successful.

Register the task

Create src/runlane.ts and add the task to its named catalog:

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

import { generateInvoice } from './tasks/generate-invoice.js'

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

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

Use the catalog handle at trigger sites. It keeps payload inference tied to the registered definition.

In the application module that accepts the request, import the shared runtime and trigger the registered task:

src/invoices/start-invoice.ts
import { runlane } from '../runlane.js'

export async function triggerInvoice(accountId: string) {
  return runlane.trigger(runlane.tasks.generateInvoice, { accountId })
}

export async function generateInvoiceNow(accountId: string) {
  return runlane.runNow(runlane.tasks.generateInvoice, { accountId })
}

trigger() returns after storing the run and its first delivery request. The returned run normally starts as queued; a worker or delivered wakeup executes it later. outcome distinguishes newly created work from an existing idempotency owner.

Choose queued or inline execution

MethodUse it whenWhat it does
trigger()A worker should execute the task laterStores a queued run and returns
runNow()This process should execute one attempt nowStores, claims, and runs one attempt inline

runNow() does not keep retrying or waiting in the same call. Its returned run may be succeeded, failed, retrying, or released after that one attempt. Normal workers and maintenance continue any later work.

Execute the run and read its result

For a local test or script, drain currently due work and then read the stored run:

scripts/try-invoice-locally.ts
import { runlane } from '../src/runlane.js'

const { run } = await runlane.trigger(runlane.tasks.generateInvoice, {
  accountId: 'account-123',
})
await runlane.drain()

const storedRun = await runlane.runs.get(runlane.tasks.generateInvoice, run.id)
process.stdout.write(`${storedRun?.output?.invoiceId ?? 'No invoice created'}\n`)

await runlane.close()

Passing the task handle to runs.get() makes the read typed. Runlane checks the task id and parses stored JSON with the current output schema.

Use runs.get(run.id) for a task-agnostic operator read. Its output is JsonValue | undefined.

In an application, keep the long-running worker or transport consumer in the process entry point rather than starting one per trigger.

Check the result

A successful integration has one stored run. Its taskId matches the task, its status becomes succeeded, and its output matches the handler result.

If triggering fails with ValidationFailed, compare the payload with the task schema. Runlane has not created a partial run. If a worker reports TaskNotFound, make sure the producer and worker registered the same task catalog.

Next, run workers for the execution path.

On this page