Skip to content
Français

Handle errors

Read dispatch errors and workflow failures, then decide what can be retried.

The error you receive depends on the operation. A dispatch rejects its promise when the agent fails; a workflow normally returns a result containing its failed tasks. Use the table below to choose how to handle each call.

CallOn failure
dispatch(), sandbox.dispatch()Rejects with an OutpostError.
createSandbox()Rejects with an OutpostError.
sandbox.command()Resolves with status, even non-zero. Rejects when stopped: code timeout after deadlineMs, aborted when the sandbox closes.
defineCommandTask()Fails its task with code process on a non-zero exit.
workflow.start()Resolves with status and errors, also when you cancel it ("cancelled"). result.unwrap() throws a WorkflowFailure unless status is "done".
steering.send()Rejects with code steering when the instruction is not delivered.
dispatch() or sandbox.command() cancelled with your own signalRejects with the signal’s reason, as passed to abort(), not an OutpostError.

Catch an OutpostError to read its code, message and recovery information. Keep other errors visible by throwing them again; recovery details help locate work retained after a failed dispatch.

import { OutpostError, dispatch, recoveryDetails } from "@elie-laloum/outpost";
import { coder, repository, sandboxProvider } from "./outpost.config.ts";

try {
  await dispatch({
    repository,
    sandboxProvider,
    agent: coder,
    branch: { mode: "named", name: "outpost/lint-fix" },
    brief: { text: "Fix the lint errors and commit the change." },
  });
} catch (error) {
  if (!(error instanceof OutpostError)) throw error;
  console.error(error.code, error.message);
  console.error(recoveryDetails(error));
}

API reference: OutpostError.

Use the recovery information to find work that was preserved after a failure.

API reference: recoveryDetails.

Recover work shows how to use these locations.

API reference: FaultCode.

quotaFault(error) and unavailableFault(error) search the error and up to seven wrapped causes. Each returns undefined when the failure is something else.

import { quotaFault, unavailableFault } from "@elie-laloum/outpost";

function classify(error: unknown): string {
  const quota = quotaFault(error);
  if (quota) return `Usage limit, resets at ${quota.resetAt ?? "unknown"}`;
  const outage = unavailableFault(error);
  if (outage) return `Service unavailable: ${outage.message}`;
  return "Other failure";
}

An outage keeps its process, provider or timeout code. Use unavailableFault() to recognize it, not the code. Quota pauses explains which signals count as a quota.

A connection timeout keeps the code timeout. When a CLI agent last reported a connection failure, details.agentDiagnostic is "connection" and unavailableFault() treats it as an outage. Diagnostics shows how to check the endpoint.

A failing task does not make start() reject. Read status and errors, or call unwrap() to throw.

import { defineTask, OutpostError } from "@elie-laloum/outpost";

export const deploy = defineTask({
  key: "deploy",
  perform: () => {
    throw new OutpostError("provider", "Deployment returned HTTP 502", {
      status: 502,
    });
  },
});
import { reportValue } from "./reporter.ts";
import {
  defineWorkflow,
  WorkflowFailure,
  OutpostError,
} from "@elie-laloum/outpost";
import { deploy } from "./deploy.ts";

export const result = await defineWorkflow("release", [deploy]).start();
try {
  result.unwrap();
} catch (error) {
  if (!(error instanceof WorkflowFailure)) throw error;
  const [first] = error.result.errors;
  if (first instanceof OutpostError) reportValue(first.code, first.details);
  // Example output: provider { status: 502 }
}

WorkflowFailure.cause is the first entry of errors, so quotaFault() and unavailableFault() work on it directly. unwrap() also throws for "paused", "waiting-input" and "cancelled" runs.

API reference: WorkflowFailure, WorkflowBudgetExceeded, WorkflowUsageUnavailable, LoopTaskExhausted, ResponseError, ReplayDivergence and TransportConflict.

A task retries only when it has a retry policy; accepts chooses which errors qualify; without it, every failure is retried. Retry outages and timeouts; do not retry configuration, prompt or conflict.

import { defineTask, unavailableFault } from "@elie-laloum/outpost";

const report = defineTask({
  key: "report",
  retry: {
    attempts: 3,
    delayMs: 1_000,
    backoff: "exponential",
    accepts: (error) => unavailableFault(error) !== undefined,
  },
  perform: () => "Replace with your request",
});

A retry runs the whole task again and can repeat its effects. Concurrency, retries and timeouts covers the options and Retry-After.

Log code, message and recoveryDetails(error). Do not dump details or the whole error: a failed process carries the agent’s stdout and stderr, and ResponseError.raw holds its answer. Both can contain repository content or secrets the agent printed.

For the full record of a failed dispatch, open its journal from the logReference recovery field.

  • Some failures are plain Errors: invalid task or workflow definitions, and a checkpoint already owned by another runner.
  • A timeout does not prove that external effects were rolled back. Check the retained branch before running again.

API: OutpostError · FaultCode · recoveryDetails · quotaFault · unavailableFault · WorkflowFailure · WorkflowResult · ResponseError · TransportConflict