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.