Handle Orchard errors
Orchard surfaces failures as typed errors instead of generic strings so callers can branch on cause instead of parsing messages. This guide covers the error types task and workflow code will see, plus the retry options that shape whether a failure is retried at all.
NonRetryableHarnessError
Defined in packages/core/src/harness.ts. The built-in harnesses rethrow auth/usage-shaped failures as NonRetryableHarnessError so the in-process runtime does not burn retry attempts on failures that will never succeed on retry:
cursor()rethrows non-retryableCursorCliErrors (auth failures, unknown CLI flags, missing binary) asNonRetryableHarnessError.codex()rethrows auth/invalid-model-shaped SDK errors the same way, and rethrows immediately if@openai/codex-sdkfails to import (it is an optional dependency).claude()rethrows non-retryableClaudeCliErrors (auth failures, usage or invalid-model failures, missing binary) asNonRetryableHarnessError.
import { NonRetryableHarnessError } from '@snevins/orchard-core/harness'
try {
await harnessCall(ctx, 'review', harness, prompt, schema, opts)
} catch (err) {
if (err instanceof NonRetryableHarnessError) {
// Fix auth, install the SDK, or correct CLI flags — do not retry as-is.
}
throw err
}See Troubleshooting — harness auth failures for the underlying CLI error text.
HarnessStructuredOutputError
Defined in packages/core/src/primitives.ts. Thrown by harnessCall when the harness exhausts structured-output repair attempts. The message keeps the last output truncated for logs, while the error fields retain the full details for programmatic handling:
import { HarnessStructuredOutputError } from '@snevins/orchard-core/primitives'
try {
await harnessCall(ctx, 'review', harness, prompt, schema, opts)
} catch (err) {
if (err instanceof HarnessStructuredOutputError) {
err.harnessName
err.lastError
err.lastOutput
err.attempts
}
throw err
}WorkflowArtifactError
Defined in packages/cli/src/workflow-artifacts.ts, thrown by CLI artifact generation and preview internals. It carries a retryable: boolean flag (check with isRetryableWorkflowArtifactError(err)) and an optional machine-readable reason:
reason | Meaning |
|---|---|
'not_found' | The manifest has no entry for the requested UUID or path. |
'no_source_hash' | The manifest entry predates sourceHash tracking (legacy or hand-authored metadata). |
'source_missing' | The manifest points at an artifact file that no longer exists on disk. |
'hash_mismatch' | The artifact file on disk no longer matches the manifest's recorded sourceHash (drifted). |
'invalid_manifest' | manifest.json itself failed to parse or validate. |
Public callers normally see this through the CLI result instead of importing the internal class. For example, orchard generate --json surfaces retryable artifact errors through the CLI; terminal artifact errors should be fixed by changing the prompt or artifact source rather than blindly retried.
Repository tests for artifact internals may import directly from packages/cli/src/workflow-artifacts.ts. External consumers should use the orchard generate and orchard preview surfaces. See Troubleshooting.
ChildTaskFailedError
Defined in packages/core/src/primitives.ts. Thrown by spawnAndAwaitTask and runReadOnlyParallelTasks when an awaited child task finishes failed or cancelled and the caller did not opt into run.failureMode: 'snapshot'. It carries the typed snapshot so callers can branch on failure cause without re-parsing Error.message:
import { ChildTaskFailedError } from '@snevins/orchard-core/primitives'
try {
await spawnAndAwaitTask(app, ctx, tasks.review, params, {
run: { stepName: 'review' },
})
} catch (err) {
if (err instanceof ChildTaskFailedError) {
err.taskName // the child TaskRef's name
err.snapshot // { state: 'failed', failure } or { state: 'cancelled' }
}
throw err
}A non-terminal snapshot (still pending/running/sleeping after awaitTaskResult returns, which should not normally happen) throws a plain Error, not ChildTaskFailedError — only failed and cancelled snapshots are typed this way.
runReadOnlyParallelTasks collects sibling failures instead of throwing on the first one: if more than one child fails, it throws an AggregateError wrapping all captured errors rather than only the first. Error messages include the failing parallel invocation id, and unsnapshotted child failures remain ChildTaskFailedError instances for callers that branch with instanceof.
failureMode: 'snapshot'
Set run.failureMode: 'snapshot' (on spawnAndAwaitTask's options.run, or on the batch run — not per-task run — for runReadOnlyParallelTasks) to receive failed/cancelled child snapshots as data instead of a thrown ChildTaskFailedError:
const result = await spawnAndAwaitTask(app, ctx, tasks.review, params, {
run: { stepName: 'review', failureMode: 'snapshot' },
})
if (result.state === 'failed') {
// result.failure is JsonValue | null — the child task failure payload
} else if (result.state === 'cancelled') {
// no further payload
} else {
// result is the parsed, typed child task result
}This is data modeling for expected child failure, not error recovery — timeouts from awaitTaskResult are never converted into snapshots and still throw. failureMode: 'snapshot' is also not rollback or retry; it only changes whether a terminal failure surfaces as a thrown error or a returned value. See packages/core/README.md ("Child failure modes").
Retry attempts
execution.retry (registration-time, via task) and run.retry (per-spawn, via spawnAndAwaitTask/runReadOnlyParallelTasks) carry a single field:
| Field | Type | Notes |
|---|---|---|
maxAttempts | number (positive integer) | Total attempts, including the first. Default 1. |
An attempt that fails is retried immediately — there is no backoff, and the in-process runtime has no cancellation API. retryStrategy and cancellation options were removed along with the database-backed runtime; because these schemas are .strict(), passing them now fails validation loudly rather than being silently ignored.
Steps completed via ctx.step are memoized for the lifetime of the task run, so a retried attempt skips work that already succeeded. That memo lives in process memory only: if the process exits, it is gone.
retryOptionsSchema lives in packages/core/src/primitives.ts.