Trigger child tasks
Create linked work from a task without hiding its lifecycle.
Use context.trigger() when one task needs to create another durable run. The child records its source run and inherits the parent's trace carrier unless you override it.
Trigger the child
Keep the related task definitions together in src/tasks/document-tasks.ts:
import { task } from '@runlane/core'
import * as z from 'zod'
const env = z.object({ DOCUMENTS_API_URL: z.url() }).parse(process.env)
export const indexDocument = task({
id: 'documents.index',
schema: z.object({ documentId: z.string() }),
async run({ documentId }, context) {
const response = await fetch(new URL(`/documents/${encodeURIComponent(documentId)}/index`, env.DOCUMENTS_API_URL), {
method: 'POST',
signal: context.signal,
})
if (!response.ok) throw new Error(`Documents API returned ${response.status}`)
},
})
export const importDocument = task({
id: 'documents.import',
schema: z.object({ documentId: z.string() }),
async run({ documentId }, context) {
const response = await fetch(
new URL(`/documents/${encodeURIComponent(documentId)}/import`, env.DOCUMENTS_API_URL),
{
method: 'POST',
signal: context.signal,
},
)
if (!response.ok) throw new Error(`Documents API returned ${response.status}`)
await context.trigger(indexDocument, { documentId }, { idempotencyKey: `index.${documentId}` })
},
})Register both tasks in the shared runtime:
import { createRunlane, queue } from '@runlane/core'
import { createLocalLane } from '@runlane/lane-local'
import { importDocument, indexDocument } from './tasks/document-tasks.js'
const defaultQueue = queue({ name: 'default', default: true })
export const runlane = createRunlane({
lane: createLocalLane(),
queues: [defaultQueue],
tasks: { importDocument, indexDocument },
})Every runtime that can execute either task needs the same catalog.
The child trigger returns after storing the child run. It does not wait for the child handler to finish.
Make replay safe
The parent handler can run again. Give the child trigger an idempotency key when another parent attempt must reuse the same child.
The key in this example is stable for the document. For repeated imports of different document versions, include the version in the key.
Wait only when the parent truly depends on the child
context.waitForRun(childRunId, { timeout }) can release a parent until a known child becomes terminal.
After resume, the parent starts from the beginning. It must reload the same child id and inspect durable state before deciding what comes next.
Runlane does not provide a built-in triggerAndWait(), durable Promise.all(), or batch result collector. Promise.all() inside one handler is same-attempt concurrency, not durable child coordination.
When several children must be joined, keep join state in your application or model the aggregation as another explicit task.
Inspect the relationship
Operator reads can filter by sourceRunId. The child run also records a trigger source link. Keep that link by using context.trigger() rather than creating unrelated work through another runtime.
Verify the flow
- Retry the parent and confirm the idempotency key returns the same child.
- Confirm the child has the expected source run id and trace carrier.
- Confirm the parent can finish independently when no join is required.
- Test timeout and terminal child states when using
waitForRun(). - Test child queue and concurrency policy separately from the parent.
Read wait without failing before adding a child join.