Orchestration Guide
Orchestration is authored in code. You define tasks and workflows in TypeScript, pick
an adapter, and then use the runtime either directly or through the
createOrchestrationPackage() wrapper inside a Slingshot app.
The shortest working path
Section titled “The shortest working path”- Define tasks with Zod input and output schemas.
- Compose them into a workflow with
step(),parallel(), andsleep(). - Pick an adapter.
- Use
createOrchestrationRuntime()directly orcreateOrchestrationPackage()inside a Slingshot app. - Trigger work from app code with
runtime.runTask()/runtime.runWorkflow(). - Add protected HTTP routes only if you actually want remote orchestration control.
Define tasks
Section titled “Define tasks”import { z } from 'zod';import { defineTask } from '@lastshotlabs/slingshot-orchestration-engine';
export const capturePayment = defineTask({ name: 'capture-payment', input: z.object({ invoiceId: z.string(), amountCents: z.number().int().positive(), }), output: z.object({ captured: z.boolean(), paymentId: z.string(), }), retry: { maxAttempts: 3, backoff: 'exponential', delayMs: 250, maxDelayMs: 2_000 }, timeout: 5_000, async handler(input, ctx) { ctx.reportProgress({ percent: 25, message: 'authorizing payment' }); return { captured: true, paymentId: `pay_${input.invoiceId}_${ctx.runId}`, }; },});Compose a workflow
Section titled “Compose a workflow”import { z } from 'zod';import { type AnyResolvedTask, defineWorkflow, parallel, step, stepResult,} from '@lastshotlabs/slingshot-orchestration-engine';
declare const capturePayment: AnyResolvedTask;declare const generateInvoice: AnyResolvedTask;declare const recordLedgerEntry: AnyResolvedTask;declare const sendReceipt: AnyResolvedTask;
export const processInvoiceWorkflow = defineWorkflow({ name: 'process-invoice', input: z.object({ invoiceId: z.string(), customerEmail: z.string().email(), amountCents: z.number().int().positive(), }), output: z.object({ payment: z.object({ captured: z.boolean(), paymentId: z.string() }), invoice: z.object({ invoiceUrl: z.string().url() }), ledger: z.object({ recorded: z.boolean() }), receipt: z.object({ delivered: z.boolean() }), }), outputMapper(results) { return { payment: stepResult(results, 'capture-payment', capturePayment)!, invoice: stepResult(results, 'generate-invoice', generateInvoice)!, ledger: stepResult(results, 'record-ledger-entry', recordLedgerEntry)!, receipt: stepResult(results, 'send-receipt', sendReceipt)!, }; }, steps: [ step('capture-payment', capturePayment), parallel([ step('generate-invoice', generateInvoice), step('record-ledger-entry', recordLedgerEntry), ]), step('send-receipt', sendReceipt), ],});Direct runtime composition
Section titled “Direct runtime composition”Use the runtime directly in workers, scripts, tests, or plain services:
import { type AnyResolvedTask, type AnyResolvedWorkflow, createMemoryAdapter, createOrchestrationRuntime,} from '@lastshotlabs/slingshot-orchestration-engine';
declare const capturePayment: AnyResolvedTask;declare const generateInvoice: AnyResolvedTask;declare const recordLedgerEntry: AnyResolvedTask;declare const sendReceipt: AnyResolvedTask;declare const processInvoiceWorkflow: AnyResolvedWorkflow;
const runtime = createOrchestrationRuntime({ adapter: createMemoryAdapter({ concurrency: 10 }), tasks: [capturePayment, generateInvoice, recordLedgerEntry, sendReceipt], workflows: [processInvoiceWorkflow],});
const handle = await runtime.runWorkflow(processInvoiceWorkflow, { invoiceId: 'inv_123', customerEmail: 'customer@example.com', amountCents: 4_200,});
const result = await handle.result();If you swap to BullMQ later, direct runtime composition stays the same. The BullMQ adapter now
lazily starts on first use, so you do not need a separate manual start() call before
runTask() / runWorkflow().
Slingshot plugin composition
Section titled “Slingshot plugin composition”Use the plugin wrapper when you want the orchestration runtime published into app context and, optionally, exposed over protected HTTP routes.
import type { MiddlewareHandler } from 'hono';import { createOrchestrationPackage } from '@lastshotlabs/slingshot-orchestration';import type { AnyResolvedTask, AnyResolvedWorkflow,} from '@lastshotlabs/slingshot-orchestration-engine';import { createMemoryAdapter } from '@lastshotlabs/slingshot-orchestration-engine';
declare const capturePayment: AnyResolvedTask;declare const generateInvoice: AnyResolvedTask;declare const recordLedgerEntry: AnyResolvedTask;declare const sendReceipt: AnyResolvedTask;declare const processInvoiceWorkflow: AnyResolvedWorkflow;
const requireOperationsKey: MiddlewareHandler = async (c, next) => { if (c.req.header('x-ops-key') !== 'dev-ops-key') { return c.json({ error: 'forbidden' }, 403); } c.set('tenantId', 'tenant-demo'); await next();};
const orchestrationPlugin = createOrchestrationPackage({ adapter: createMemoryAdapter({ concurrency: 10 }), tasks: [capturePayment, generateInvoice, recordLedgerEntry, sendReceipt], workflows: [processInvoiceWorkflow], routes: true, routePrefix: '/orchestration', routeMiddleware: [requireOperationsKey],});Trigger work from app code
Section titled “Trigger work from app code”Other plugins can read the runtime from pluginState through getOrchestration() and start work
without going through the HTTP API:
import { type SlingshotPlugin, getContext } from '@lastshotlabs/slingshot';import { ORCHESTRATION_PLUGIN_STATE_KEY, getOrchestration,} from '@lastshotlabs/slingshot-orchestration';import type { AnyResolvedWorkflow } from '@lastshotlabs/slingshot-orchestration-engine';
declare const processInvoiceWorkflow: AnyResolvedWorkflow;
export function createBillingApiPlugin(): SlingshotPlugin { return { name: 'billing-api', dependencies: [ORCHESTRATION_PLUGIN_STATE_KEY], setupRoutes({ app }) { app.post('/billing/invoices/:invoiceId/process', async c => { const runtime = getOrchestration(getContext(app)); const invoiceId = c.req.param('invoiceId');
const handle = await runtime.runWorkflow(processInvoiceWorkflow, { invoiceId, customerEmail: 'customer@example.com', amountCents: 4_200, });
return c.json({ runId: handle.id }, 202); }); }, };}That pattern is the cleanest way to keep orchestration internal to your app while still letting feature routes trigger background work.
HTTP routes are optional
Section titled “HTTP routes are optional”Enable orchestration HTTP routes only when you want external systems or admin tools to trigger runs.
When routes are enabled, routeMiddleware must be non-empty.
Current route surface:
POST /orchestration/tasks/:name/runsPOST /orchestration/workflows/:name/runsGET /orchestration/runs/:idDELETE /orchestration/runs/:idGET /orchestration/runsPOST /orchestration/runs/:id/signal/:signalName
Adapter choice
Section titled “Adapter choice”createMemoryAdapter()for local development and testscreateSqliteAdapter()for durable single-node executioncreateBullMQOrchestrationAdapter()for Redis-backed queues, scheduling, and worker fleets
Source-backed reference
Section titled “Source-backed reference”The repo includes a runnable example app at examples/orchestration/.
See Orchestration Example for the exact source-backed app composition.