Skip to main content

Standalone tasks

A standalone task is the simplest unit of work. Use hatchet.task() to create one — it behaves like a workflow with a single step and exposes run(), runNoWait(), schedule(), and cron() directly.
workflow.ts
import { hatchet } from './hatchet-client';

type SimpleInput = {
  Message: string;
};

export const simple = hatchet.task({
  name: 'simple',
  retries: 3,
  fn: async (input: SimpleInput) => {
    return {
      TransformedMessage: input.Message.toLowerCase(),
    };
  },
});

Task options

name
string
required
The name of the task. Used as the identifier in the Hatchet dashboard and as the key for parent output lookups in DAGs.
fn
(input: I, ctx: Context<I>) => O | Promise<O>
The function to execute. Receives the workflow input and a Context instance. The return value becomes the task output.
retries
number
default:"0"
Number of times to retry the task on failure.
executionTimeout
Duration
default:"\"60s\""
Maximum wall-clock time the task function is allowed to run. Accepts Go duration strings such as "30s", "5m", "1h".
scheduleTimeout
Duration
default:"\"5m\""
Maximum time the task is allowed to wait in the queue before it starts. Accepts Go duration strings.
backoff
{ factor?: number; maxSeconds?: number }
Exponential backoff configuration for retries. factor is the base of the exponent and maxSeconds caps the delay.
rateLimits
RateLimit[]
Rate limit keys consumed when this task runs. Each entry accepts units, key, staticKey, dynamicKey, limit, and duration.
concurrency
Concurrency | Concurrency[]
Concurrency controls for this task. See Concurrency below.
desiredWorkerLabels
Record<string, { value: string | number; required?: boolean; weight?: number; comparator?: WorkerLabelComparator }>
Worker label requirements for routing this task to specific workers.

Durable tasks

Durable tasks survive worker restarts and support long-running waits backed by Hatchet’s durable event log. Create them with hatchet.durableTask() or workflow.durableTask(). The task function receives a DurableContext instead of a plain Context. DurableContext extends Context and adds:
  • ctx.sleepFor(duration) — pause execution for a durable duration (survives restarts)
  • ctx.sleepUntil(date) — pause until a specific timestamp
  • ctx.waitFor(conditions) — pause until one or more conditions are met
  • ctx.waitForEvent(key, expression?, schema?) — pause until a named user event arrives
  • ctx.now() — get a memoized current timestamp (consistent across replays)
  • ctx.spawnChild(workflow, input, opts?) — spawn a child workflow durably
  • ctx.spawnChildren(children) — spawn multiple child workflows durably
import { hatchet } from './hatchet-client';

export const waitForApproval = hatchet.durableTask({
  name: 'wait-for-approval',
  executionTimeout: '24h',
  fn: async (input: { orderId: string }, ctx) => {
    // Pause until the 'order:approved' event arrives for this order
    const event = await ctx.waitForEvent(
      'order:approved',
      `input.orderId == "${input.orderId}"`
    );

    return { approved: true, event };
  },
});
Durable tasks can also be added to a workflow using workflow.durableTask():
const wf = hatchet.workflow({ name: 'approval-flow' });

wf.durableTask({
  name: 'await-approval',
  executionTimeout: '24h',
  fn: async (input, ctx) => {
    await ctx.sleepFor('5m');
    const event = await ctx.waitForEvent('order:approved');
    return { event };
  },
});

Multi-step workflows (DAGs)

Use hatchet.workflow() to declare a workflow and then add tasks to it. Tasks can declare parents to form a directed acyclic graph (DAG).
workflow.ts
import { hatchet } from './hatchet-client';

type DagInput = { Message: string };

type DagOutput = {
  reverse: {
    Original: string;
    Transformed: string;
  };
};

export const dag = hatchet.workflow<DagInput, DagOutput>({
  name: 'simple',
});

// First task — no parents, runs immediately
const toLower = dag.task({
  name: 'to-lower',
  fn: (input) => ({
    TransformedMessage: input.Message.toLowerCase(),
  }),
});

// Second task — runs after toLower completes
dag.task({
  name: 'reverse',
  parents: [toLower],
  fn: async (input, ctx) => {
    const lower = await ctx.parentOutput(toLower);
    return {
      Original: input.Message,
      Transformed: lower.TransformedMessage.split('').reverse().join(''),
    };
  },
});

Workflow options

name
string
required
The name of the workflow.
description
string
A human-readable description shown in the Hatchet dashboard.
version
string
An optional version string for the workflow.
on
{ cron?: string | string[]; event?: string | string[] }
Shorthand for declaring cron and event triggers. Equivalent to onCrons / onEvents.
onCrons
string[]
Cron expressions that automatically trigger the workflow. See Cron triggers.
onEvents
string[]
Event keys that automatically trigger the workflow. See Event triggers.
concurrency
Concurrency | Concurrency[]
Workflow-level concurrency controls. See Concurrency.
defaultPriority
Priority
Default priority for runs of this workflow. Values: Priority.LOW (1), Priority.MEDIUM (2), Priority.HIGH (3).
sticky
StickyStrategy
Sticky strategy for scheduling: StickyStrategy.SOFT or StickyStrategy.HARD.
taskDefaults
TaskDefaults
Default executionTimeout, scheduleTimeout, retries, backoff, rateLimits, workerLabels, and concurrency applied to all tasks in this workflow. Individual tasks can override these values.
inputValidator
z.ZodType
A Zod schema for the workflow input. When provided, a JSON Schema is sent to the Hatchet backend for dashboard autocomplete.

Task context

The Context object is passed as the second argument to every task function.

Accessing parent outputs

dag.task({
  name: 'summarize',
  parents: [toLower],
  fn: async (input, ctx) => {
    // Pass the task options object or the task name string
    const lower = await ctx.parentOutput(toLower);
    return { summary: lower.TransformedMessage };
  },
});
ctx.parentOutput(task) accepts either a CreateWorkflowTaskOpts reference or a task name string and returns the typed output of the parent task.

Logging

fn: async (input, ctx) => {
  await ctx.logger.info('Processing started');
  await ctx.logger.debug('Input received', input);
  await ctx.logger.warn('Retrying...', { error: new Error('timeout') });
  await ctx.logger.error('Unexpected failure', { error: err });
},
Log lines appear in the Hatchet dashboard under the task run.

Other context methods

MethodReturnsDescription
ctx.workflowRunId()stringID of the current workflow run.
ctx.taskRunExternalId()stringID of the current task run.
ctx.taskName()stringName of the currently executing task.
ctx.workflowName()stringName of the current workflow.
ctx.retryCount()numberNumber of times this task has been retried.
ctx.additionalMetadata()Record<string, string>Metadata attached to the workflow run.
ctx.errors()Record<string, string>Task run errors — use in onFailure handlers.
ctx.triggeredByEvent()booleanWhether the workflow was triggered by an event.
ctx.filterPayload()Record<string, any>Payload from the matched event filter.
ctx.cancelledbooleanWhether the task has been cancelled.
ctx.abortControllerAbortControllerAbort controller for the task run.
ctx.refreshTimeout(duration)Promise<void>Extend the execution timeout by duration.
ctx.releaseSlot()Promise<void>Release the worker slot while the task continues.
ctx.putStream(data)Promise<void>Stream data from the task.
ctx.runChild(workflow, input, opts?)Promise<P>Run a child workflow and wait for its result.
ctx.runNoWaitChild(workflow, input, opts?)Promise<WorkflowRunRef<P>>Enqueue a child workflow without waiting.
ctx.bulkRunChildren(children)Promise<P[]>Run multiple child workflows in parallel, wait for all.
ctx.bulkRunNoWaitChildren(children)Promise<WorkflowRunRef<P>[]>Enqueue multiple child workflows without waiting.

Lifecycle hooks

Workflows support onFailure and onSuccess handlers that run when all tasks fail or all tasks succeed:
const wf = hatchet.workflow({ name: 'my-workflow' });

wf.task({
  name: 'main',
  fn: async (input) => ({ result: 'ok' }),
});

wf.onFailure({
  fn: async (input, ctx) => {
    const errors = ctx.errors();
    console.error('Workflow failed:', errors);
  },
});

wf.onSuccess({
  fn: async (input, ctx) => {
    console.log('Workflow succeeded!');
  },
});

Triggering runs

.run(input, opts?)

Triggers a run and waits for the result. Accepts a single input object or an array for bulk execution.
// Single run
const result = await simple.run({ Message: 'Hello' });
console.log(result.TransformedMessage); // "hello"

// Bulk run
const results = await simple.run([
  { Message: 'Hello' },
  { Message: 'World' },
]);

.runNoWait(input, opts?)

Enqueues a run and immediately returns a WorkflowRunRef without blocking.
const ref = await simple.runNoWait({ Message: 'Hello' });
const result = await ref.result();

Run options

additionalMetadata
Record<string, string>
Arbitrary key-value metadata attached to the run and visible in the dashboard.
priority
Priority
Run priority: Priority.LOW, Priority.MEDIUM, or Priority.HIGH.
sticky
boolean
When true and called from within a parent task, routes the run to the same worker.
childKey
string
A deduplication key when spawning from a parent task.
returnExceptions
boolean
When true and input is an array, failures are returned as Error instances instead of rejecting the whole array.

.schedule(enqueueAt, input, opts?)

Schedules a one-off run at a specific Date.
const scheduled = await simple.schedule(
  new Date('2026-01-01T00:00:00Z'),
  { Message: 'Happy New Year' }
);

.delay(durationSeconds, input, opts?)

Schedules a run to trigger after a delay in seconds.
const scheduled = await simple.delay(60, { Message: 'Delayed' });

.cron(name, expression, input, opts?)

Creates a named cron schedule that triggers the task on a recurring basis.
const cronJob = await simple.cron(
  'nightly-report',
  '0 0 * * *',
  { Message: 'Nightly' }
);

Event triggers

Workflows can be triggered automatically when events are pushed to Hatchet. Use onEvents (or the on.event shorthand):
export const lower = hatchet.workflow({
  name: 'lower',
  onEvents: ['simple-event:create'],
});

lower.task({
  name: 'lower',
  fn: (input) => ({
    TransformedMessage: input.Message.toLowerCase(),
  }),
});
The shorthand on field accepts both event and cron triggers:
export const upper = hatchet.workflow({
  name: 'upper',
  on: {
    event: 'simple-event:create',
  },
});
To push an event programmatically:
await hatchet.events.push('simple-event:create', { Message: 'Hello' });

Cron triggers

Declare a recurring cron schedule directly on the workflow:
export const onCron = hatchet.workflow({
  name: 'on-cron-workflow',
  on: {
    cron: '*/15 * * * *', // every 15 minutes
  },
});

onCron.task({
  name: 'job',
  fn: (input) => ({
    TransformedMessage: input.Message.toLowerCase(),
  }),
});

Concurrency

Both workflows and tasks accept a concurrency option to limit how many runs can execute simultaneously.
import { ConcurrencyLimitStrategy } from '@hatchet-dev/typescript-sdk';

export const ordersWorkflow = hatchet.workflow({
  name: 'process-order',
  concurrency: {
    // Key by the group field — one run per group at a time
    expression: 'input.group',
    maxRuns: 1,
    limitStrategy: ConcurrencyLimitStrategy.CANCEL_IN_PROGRESS,
  },
});

Concurrency options

expression
string
required
A CEL expression evaluated against the workflow input to produce the concurrency key. For example, "input.userId" groups runs by user.
maxRuns
number
default:"1"
Maximum number of concurrent runs that share the same concurrency key.
limitStrategy
ConcurrencyLimitStrategy
default:"CANCEL_IN_PROGRESS"
Strategy when the limit is reached. Options:
  • ConcurrencyLimitStrategy.CANCEL_IN_PROGRESS — cancel the currently running run
  • ConcurrencyLimitStrategy.CANCEL_NEWEST — cancel the incoming run
  • ConcurrencyLimitStrategy.GROUP_ROUND_ROBIN — round-robin across concurrency groups
  • ConcurrencyLimitStrategy.DROP_NEWEST — drop the incoming run (deprecated)
  • ConcurrencyLimitStrategy.QUEUE_NEWEST — queue the new run, cancel the oldest queued run (deprecated)
Pass an array to apply multiple concurrency keys simultaneously:
concurrency: [
  { expression: 'input.tenantId', maxRuns: 10 },
  { expression: 'input.userId', maxRuns: 2 },
]

Build docs developers (and LLMs) love