executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
hatchetOrchestrate background jobs and workflows with Hatchet — open-source distributed task queue with DAG workflows. Use when someone asks to "run background jobs", "Hatchet", "workflow orchestration", "distributed task queue", "durable execution", "replace Celery/Bull", or "DAG workflow engine". Covers workflow definition, step functions, retries, concurrency c…
Independent GitHub snapshot · Automated classification · Original documentation governs
Hatchet is an open-source task queue and workflow engine on top of Postgres. You declare tasks (single functions) or workflows (DAGs of tasks), run workers that pick them up, and trigger runs from code, events, schedules or the dashboard. It gives per-task retries with backoff, timeouts, CEL-based concurrency control, rate limits and durable tasks. Typical uses: payment processing, data pipelines, AI agent orchestration, replacing BullMQ or Celery.
This skill uses the v1 SDK (@hatchet-dev/typescript-sdk 1.x, checked against 1.34.0 and engine v0.107). The old hatchet.workflow({ on: ... }).step(...) / ctx.stepOutput() API is the legacy v0 style; do not write new code with it.
npm install @hatchet-dev/typescript-sdk # Python: pip install hatchet-sdk
# create the token in the dashboard (Settings > API Tokens), then:
export HATCHET_CLIENT_TOKEN="$(cat ~/.config/hatchet/worker-token)"
Hatchet Cloud or a self-hosted engine both issue that token. The CLI is available with brew install hatchet-dev/hatchet/hatchet --cask; hatchet profile add stores the token, hatchet worker dev runs a worker with hot reload, hatchet trigger simple fires a run of the quickstart workflow. For self-hosting follow docs.hatchet.run/self-hosting (Docker Compose runs the engine, dashboard on http://localhost:8080 and Postgres; the compose file's default login is for local use only, change it).
// src/hatchet-client.ts
import { HatchetClient } from '@hatchet-dev/typescript-sdk';
export const hatchet = HatchetClient.init(); // reads HATCHET_CLIENT_TOKEN
// src/tasks/send-invoice.ts
import { hatchet } from '../hatchet-client';
export const sendInvoice = hatchet.task({
name: 'send-invoice',
retries: 3,
backoff: { factor: 2, maxSeconds: 30 }, // waits 2s, 4s, 8s...
executionTimeout: '1m',
fn: async (input: { orderId: string; email: string }, ctx) => {
ctx.logger.info(`retry ${ctx.retryCount()} for ${input.orderId}`);
return { sent: true };
},
});
Parents are task objects, not strings, and a child reads parent output with await ctx.parentOutput(task).
// src/workflows/onboarding.ts
import { hatchet } from '../hatchet-client';
type OnboardingInput = { userId: string; email: string };
export const onboarding = hatchet.workflow<OnboardingInput>({
name: 'user-onboarding',
onEvents: ['user:created'], // event trigger
});
const welcome = onboarding.task({
name: 'send-welcome-email',
retries: 3,
executionTimeout: '30s',
fn: async (input) => ({ emailSent: true, to: input.email }),
});
const workspace = onboarding.task({
name: 'create-workspace',
parents: [welcome],
fn: async (input) => ({ workspaceId: `ws_${input.userId}` }),
});
onboarding.task({
name: 'send-guide',
parents: [welcome, workspace],
fn: async (input, ctx) => {
const ws = await ctx.parentOutput(workspace);
return { guideFor: ws.workspaceId };
},
});
// src/worker.ts
import { hatchet } from './hatchet-client';
import { onboarding } from './workflows/onboarding';
import { sendInvoice } from './tasks/send-invoice';
async function main() {
const worker = await hatchet.worker('billing-worker', {
workflows: [onboarding, sendInvoice],
slots: 50, // concurrent task runs this worker accepts
});
await worker.start();
}
main();
import { onboarding } from './workflows/onboarding';
import { hatchet } from './hatchet-client';
// wait for the result
const result = await onboarding.run({ userId: 'u_8412', email: 'kai@acmelabs.io' });
// fire and forget, returns a run reference
await onboarding.runNoWait({ userId: 'u_8413', email: 'mira@acmelabs.io' });
// event trigger: every workflow with onEvents: ['user:created'] starts
await hatchet.events.push('user:created', { userId: 'u_8414', email: 'ravi@acmelabs.io' });
Limits are grouped by a CEL expression over the input. Strategies: GROUP_ROUND_ROBIN (queue and share fairly), CANCEL_IN_PROGRESS, CANCEL_NEWEST, CANCEL_QUEUED_EXCEPT_NEWEST, CANCEL_QUEUED_EXCEPT_OLDEST. An array lets you stack limits.
import { ConcurrencyLimitStrategy } from '@hatchet-dev/typescript-sdk';
export const apiSync = hatchet.workflow<{ provider: string; endpoint: string }>({
name: 'api-sync',
concurrency: {
expression: 'input.provider', // at most 5 runs per provider
maxRuns: 5,
limitStrategy: ConcurrencyLimitStrategy.GROUP_ROUND_ROBIN,
},
});
export const dailyReport = hatchet.workflow({
name: 'daily-report',
on: { cron: '0 9 * * *' }, // UTC; 5 or 6 fields (optional leading seconds)
});
User prompt: "Build a reliable order workflow in TypeScript: validate the cart, charge the card, then notify the customer and the warehouse."
The agent creates hatchet.workflow<OrderInput>({ name: 'process-order' }) with a validate task, a charge task (parents: [validate], retries: 3, executionTimeout: '45s', the payment provider idempotency key set to the order id) and notify-customer and notify-warehouse tasks that both list charge as parent so they run in parallel. It registers the workflow on a worker, adds await processOrder.run(...) to the checkout handler, and the dashboard shows each task with its retry history.
User prompt: "Our scraper-then-summarize job hammers the OpenAI API. Allow only 3 runs at a time per customer."
The agent adds concurrency: { expression: 'input.customerId', maxRuns: 3, limitStrategy: ConcurrencyLimitStrategy.GROUP_ROUND_ROBIN } to the workflow, so extra runs wait in the queue instead of failing. Starting 10 runs for one customer shows 3 running and 7 queued, while another customer's runs still start immediately.
hatchet.task, hatchet.workflow + .task, parents: [taskObject], ctx.parentOutput). Docs and blog posts showing .step() and ctx.stepOutput() are the old SDK.executionTimeout and retries per task; make tasks idempotent because retries re-run them.workflows; a run that never starts usually means the worker lacks it, or has no free slots.HATCHET_CLIENT_TOKEN in the environment or a secret store, never in the repository.