Stagehand

Script

The Script class API: constructor, defineInput, addPhase, addStep, addBranch, use, outline, run, plus script(), stepFor, and routineFor.

Script<In, Ctx, Reserved, Slots, Open, Flags> is the builder every script starts from. Only the first type parameter is ever written by hand (In as new Script<Input>(...), or inferred via defineInput); the rest are inferred automatically as addStep, addPhase, use, and defineFlag are chained. Full narrative coverage of each method lives in the guides linked below — this page is the flat signature reference.

new Script(options?)

constructor(options?: ScriptOptions | string)

Accepts a ScriptOptions object, or a bare string as shorthand for { name: options }. See Script Options and Results for every field.

script(options?)

function script<In = void>(options?: ScriptOptions | string): Script<In, {}>

A factory export equivalent to new Script<In>(options), for callers who prefer not to write new.

.defineInput(schema)

defineInput<TSchema>(
  schema: StandardSchemaV1<unknown, TSchema>,
): Script<TSchema, Ctx, Reserved>

Infers In from a Standard Schema-compliant validator’s output type, and validates whatever is passed to run() against it before any phase executes, throwing SchemaValidationError on failure. Call it first, immediately after construction: any addStep/addPhase called before it still sees the previous In. See The Typed Context.

.defineFlag(options)

defineFlag<const Name extends string>(
  options: BooleanFlagOptions<Name>,
): Script<In, Ctx, Reserved, Slots, Open, Flags & Record<Name, boolean>>

defineFlag<const Name extends string, const Default extends string | undefined = undefined>(
  options: StringFlagOptions<Name, Default>,
): Script<In, Ctx, Reserved, Slots, Open, Flags & Record<Name, Default extends string ? string : string | undefined>>

interface BaseFlagOptions<Name extends string> {
  name: Name;
  long?: string;   // overrides the default, kebab-cased from `name`
  short?: string;  // single-letter `-` alias
  description?: string;
}
interface BooleanFlagOptions<Name> extends BaseFlagOptions<Name> {
  boolean: true;
  default?: boolean;
}
interface StringFlagOptions<Name, Default> extends BaseFlagOptions<Name> {
  boolean?: false;
  default?: Default;
}

Declares a flag run() parses off the command line into context.flags, keyed by name. Throws DuplicateNameError if name, the resolved long flag, or short is already used by another flag on this script. Full treatment in Flags.

.addPhase(name, options?)

addPhase(name: string, options?: PhaseOptions<In, Ctx>): Script<...>
addPhase(name: string, build: (script: Script<...>) => Script<...>): Script<...>
addPhase(name: string, options: PhaseOptions<In, Ctx>, build: (...) => Script<...>): Script<...>

Opens a new phase; subsequent addStep calls land in it until the next addPhase. The two-argument callback form groups a phase’s steps in a nested scope while preserving the same type flow as the flat form. Throws DuplicateNameError if the name is already used anywhere in the script.

PhaseOptions:

interface PhaseOptions<In, Ctx, Flags> {
  description?: string;
  when?: (context: { input: In; ctx: Ctx; flags: Flags }) => Awaitable<boolean>;
  cache?: CacheSource<In, Ctx, unknown, Flags>;
}

See Phases and Steps and Caching.

.addStep(def)

addStep<Out, RollbackKeys, CleanKeys>(
  def: StepDef<In, Ctx, Out, RollbackKeys, Slots> & CleanField<Ctx, Reserved, CleanKeys>,
): Script<...>

Appends a step to the currently open phase. StepDef:

interface StepDef<In, Ctx, Out, RollbackKeys, Slots, Flags> {
  name: string;
  description?: string;
  handler: (context: StepContext<In, Ctx, Slots, Flags>) => Awaitable<Out>;
  rollbackKeys?: RollbackKeys & readonly (keyof Merge<Ctx, Out>)[];
  rollback?: (context: RollbackContext<In, RollbackData<...>, Out, Slots, Flags>) => Awaitable<void>;
  when?: (context: { input: In; ctx: Ctx; flags: Flags }) => Awaitable<boolean>;
  cache?: CacheSource<In, Ctx, unknown, Flags>;
  retry?: RetryPolicy;
  timeoutMs?: number;
}

Plus clean?: readonly (keyof Ctx)[] via CleanField, checked element-wise against the step’s incoming context and the keys reserved by earlier rollbackKeys. Throws StepDefinitionError if clean names a reserved key, or DuplicateNameError if the step’s name is already used in its phase. See The Typed Context, Rollbacks, and Cleaning Context Keys.

StepContext<In, Ctx, Slots, Flags> (the handler’s parameter)

interface StepContext<In, Ctx, Slots, Flags> {
  readonly input: In;
  readonly ctx: Ctx;
  readonly signal: AbortSignal;
  readonly attempt: number;
  readonly phase: string;
  readonly step: string;
  log(message: string): void;
  info(message: string): void;
  warn(message: string): void;
  error(message: string): void;
  success(message: string): void;
  status(message: string): void;
  note(message: string): void;
  progress(options: { total: number; label?: string; value?: number }): ProgressHandle;
  task(label: string): TaskHandle;
  tasks<const K extends readonly string[]>(labels: K): TaskListHandle<K[number]>;
  readonly cache: CacheHandle<Slots>;
  readonly prompt: PromptHandle;
  readonly flags: Flags;
}

Full description of every member in The Handler Surface. prompt is documented on its own in Prompts, and flags in Flags.

RollbackContext<In, Ctx, Out, Slots, Flags> (the rollback’s parameter)

interface RollbackContext<In, Ctx, Out, Slots, Flags> {
  readonly input: In;
  readonly ctx: Ctx;      // only the keys named in rollbackKeys
  readonly output: Out;   // exactly what the handler returned
  readonly error: unknown;
  readonly signal: AbortSignal;
  readonly phase: string;
  readonly step: string;
  log(message: string): void;
  status(message: string): void;
  note(message: string): void;
  progress(options: { total: number; label?: string; value?: number }): ProgressHandle;
  readonly cache: CacheHandle<Slots>;
  readonly flags: Flags;
}

Full description in Rollbacks.

ProgressHandle

interface ProgressHandle {
  update(value: number, label?: string): void;
  increment(by?: number): void;
  setTotal(total: number): void;
  setLabel(label: string): void;
  done(): void;
  readonly value: number;
  readonly total: number;
}

TaskHandle / TaskListHandle<K>

interface TaskHandle {
  start(text?: string): void;
  label(text: string): void;
  succeed(text?: string): void;
  fail(text?: string): void;
  skip(text?: string): void;
}

interface TaskListHandle<K extends string> {
  readonly tasks: Record<K, TaskHandle>;
  get(key: K): TaskHandle;
}

PromptHandle

interface PromptHandle {
  text(options: TextPromptOptions): Promise<string>;
  confirm(options: ConfirmPromptOptions): Promise<boolean>;
  select<Value>(options: SelectPromptOptions<Value>): Promise<Value>;
  multiselect<Value>(options: MultiSelectPromptOptions<Value>): Promise<Value[]>;
}

Full description, including the non-interactive fallback and the option types, in Prompts.

RetryPolicy

interface RetryPolicy {
  attempts: number;
  delayMs?: number | ((attempt: number) => number);
  retryIf?: (error: unknown, attempt: number) => boolean;
}

.addBranch(def)

addBranch<Out extends object | void, const Name extends string>(
  def: BranchDef<In, Ctx, Out, Slots, Flags> & { name: Name },
): Script<In, Merge<Ctx, Out>, Reserved, Slots, Open, Flags>

interface BranchDef<In, Ctx, Out, Slots, Flags> {
  name: string;
  description?: string;
  condition: (context: StepContext<In, Ctx, Slots, Flags>) => Awaitable<boolean>;
  onTrue: (context: StepContext<In, Ctx, Slots, Flags>) => Awaitable<Out>;
  onFalse: (context: StepContext<In, Ctx, Slots, Flags>) => Awaitable<NoInfer<Out>>;
  retry?: RetryPolicy;
  timeoutMs?: number;
}

Appends a step to the open phase that runs onTrue when condition resolves truthy and onFalse otherwise. Out is inferred from onTrue; onFalse must return the same shape, so the context is typed identically for every later step whichever arm ran. It is an ordinary step at run time, so a throw from the condition or either arm fails it, and retry / timeoutMs cover the whole thing. There is no rollback, cache, clean or when. Throws DuplicateNameError if the name is already used in its phase. See Phases and Steps.

.use(source, options?)

use(routine: Routine<SubIn, Ctx, Out, R, RS>, options: { as?: string; input: MountInput<In, Ctx, SubIn, Flags> }): Script<...>
use(script: Script<SubIn, Out, R, SS, SO>, options: { as?: string; input: MountInput<In, Ctx, SubIn, Flags> }): Script<...>
use(script: Script<In, Out, R, SS, SO>, options?: { as?: string }): Script<...>
use(routine: Routine<In, Ctx, Out, R, RS>, options?: { as?: string }): Script<...>

type MountInput<In, Ctx, SubIn, Flags> =
  | SubIn
  | ((context: { input: In; ctx: Ctx; flags: Flags }) => Awaitable<SubIn>);

Splices a Routine (from routineFor) or another Script into this one. With no input option the source must already accept this script’s In/Ctx directly; with input, a fixed value or a { input, ctx, flags } => SubIn mapper feeds the mount something else, resolved once when the mount is reached — the native way to project a flag onto a routine’s input, with no manual step needed: use(routine, { input: ({ flags }) => ({ env: flags.environment }) }). as prefixes the mounted phases’ (and their cache slots’) names, required when mounting the same source twice. Throws DuplicateNameError on a name collision, or StepDefinitionError if source is neither a Routine nor a Script. Full treatment in Reusable Scripts and Mounts and Flags.

.outline()

outline(): Array<{ phase: string; steps: string[] }>

Reports the declared structure — every phase and its step names, in order — without running anything.

.run(input, options?)

run(input: In, options?: RunOptions): Promise<RunResult<Ctx>>

interface RunOptions {
  signal?: AbortSignal;
  cache?: CacheMode;        // "on" | "off" | "refresh" | "read-only"
  argv?: readonly string[]; // default process.argv.slice(2); see defineFlag
}

Executes every phase and step in order, applying defineInput validation and then defineFlag parsing first, in that order, if either was set. Resolves to a RunResult<Ctx> (see Script Options and Results), unless throwOnError is set, in which case a failing run rejects with the original error instead. A bad flag (UnknownFlagError, MissingFlagValueError) always throws directly, the same as a SchemaValidationError from a bad input.

stepFor<In, Ctx, Slots, Flags>()

function stepFor<In, Ctx extends object = {}, Slots = {}, Flags = {}>(): <Out, RollbackKeys>(
  def: StepDef<In, Ctx, Out, RollbackKeys, Slots, Flags>,
) => StepDef<In, Ctx, Out, RollbackKeys, Slots, Flags>

Binds the input and context a step declared outside any script expects, keeping full inference on its handler’s return type. The requirement is enforced when the resulting definition is passed to addStep on a real script. See Splitting Steps Across Files.

WithStepFor<Step, Rest>

type WithStepFor<
  Step extends { handler: (context: never) => unknown },
  Rest extends object = {},
>

The context a later step sees: Step’s output merged over Rest, so the next stepFor can declare its Ctx without restating an earlier step’s return shape. Nests (WithStepFor<typeof b, WithStepFor<typeof a, { rest }>>), with the outermost step winning a name collision, matching runtime shadowing. Reads only the output; the step’s own Ctx requirement is still checked at addStep. See Splitting Steps Across Files.

routineFor<In, Ctx>()

function routineFor<In, Ctx extends object = {}>(): <Out, R, S, O>(
  name: string,
  build: (script: Script<In, Ctx, never, {}, ClosedPhase, UnknownFlags>) => Script<In, Out, R, S, O>,
) => Routine<In, Ctx, Out, R, Commit<S, O>>

Declares a reusable fragment of phases, built with an ordinary Script inside the callback and recorded under name. Mount it with .use(). The builder script’s Flags is UnknownFlags, not {} — a routine is written away from any particular script and cannot know what flags the eventual host will declare, so a step added inside the callback can still read context.flags (typed loosely, as Record<string, unknown>) rather than being stuck with an unusable empty type. use() does not merge a routine’s own flags into the host’s Flags either way — see Flags.