Run context

Every target body may take one argument, ctx: TargetContext — a typed handle to the run it's executing in. A zero-arg () => … body is still valid; take ctx only when you need it. It carries run identity, a cancellation signal, this target's durable state, any external signal payloads the run has received, and whether this is a dry run.

The shape of ctx

export interface TargetContext {
  readonly runId: string;                 // stable for every target in the run (crypto.randomUUID())
  readonly target: string;                // dotted name of the executing target
  readonly signal: AbortSignal;           // aborted when the run is cancelled
  readonly state: TargetStateHandle;      // this target's durable metadata
  stateOf(target: string): TargetStateHandle;   // another target's state handle
  readonly signals: ReadonlyMap<string, SignalRecord>;  // external signal payloads
  readonly dryRun: boolean;               // true under a dry run (bodies don't execute)
}
export type TargetFn = (ctx: TargetContext) => void | Promise<void>;

runId is stable for every target in the run — the same UUID from start to finish, useful for correlating logs. target is the dotted name of the target currently executing.

import { Build, target } from "jsr:@zuke/core";

class Deploy extends Build {
  ship = target().executes(async (ctx) => {
    console.log(`run ${ctx.runId} · target ${ctx.target}`);
    await $`terraform apply`;          // SIGTERM'd if the run is cancelled (ambient signal)
    await ctx.state.set({ slot: "sit-7" });
  });
}

ctx.state

ctx.state is this target's own durable metadata — a small JSON record it can write to and read back, including across a suspend/resume boundary. Set merges a patch (it doesn't replace the whole record); get reads the current value synchronously. ctx.stateOf(target) reaches a different target's state handle — typically a dependency's, letting a later target read what an earlier one recorded.

export interface TargetStateHandle {
  set(patch: Record<string, JsonValue>): Promise<void>;  // merge a JSON patch, awaits write
  get(): Record<string, JsonValue>;                       // read current metadata
}

ship = target().executes(async (ctx) => {
  await ctx.state.set({ slot: "sit-7" });   // merge — doesn't clobber earlier keys
  const meta = ctx.state.get();             // { slot: "sit-7", ... }
});

promote = target()
  .dependsOn(this.ship)
  .executes((ctx) => {
    const shipped = ctx.stateOf("ship").get();   // read a dependency's state
    console.log(shipped.slot);
  });

With no state store configured, ctx.state is an in-memory no-op: reads and writes are consistent within the run, but nothing persists once the process exits. Opt into a real store to make it durable — see Durable run state for the store precedence, the record shape, and the zuke runs CLI.

ctx.signals

ctx.signals is a read-only map of external signal payloads the run has received — the mechanism a .waitsFor() target uses to unblock, and how a later target reads what was delivered ({ data, receivedAt }). See Suspend & resume for how a signal is delivered with zuke resume --signal and how a target waits on one.

awaitTesting = target().executes((ctx) => {
  const approval = ctx.signals.get("testing-approved");  // SignalRecord | undefined
  console.log(approval?.data, approval?.receivedAt);
});

Ambient cancellation

ctx.signal is an AbortSignal that aborts when the run is cancelled. You rarely need to pass it around by hand: the run's signal is installed as the $ shell's ambient signal (via AsyncLocalStorage, scoped per run), so a plain $\`…\` inside a target body is terminated on cancel without threading ctx.signal through every call. Override it on one command — or compose it with .killAfter() for a grace period before SIGKILL:

ship = target().executes(async (ctx) => {
  await $`terraform apply`;                                    // ambient signal — SIGTERM'd on cancel
  await $`long-migration`.signal(ctx.signal).killAfter("30s");  // explicit override, composes with killAfter
});

Zuke never forcibly interrupts arbitrary JavaScript, though: a body that ignores signal and never touches the shell runs to completion even after cancellation is requested. See Cancellation & compensation for the full picture — zuke cancel, Ctrl-C, and how compensating targets unwind completed work.

Dry runs

ctx.dryRun reports whether the current run is a dry run. In a dry run a target's body doesn't execute at all — planning stops short of invoking it, whether triggered by --dry-run on the CLI or the dryRun argument to an MCP run: tool — so the flag exists mainly to round out the context's shape rather than something a body branches on at runtime.