Skip to content

No way to end a run early and successfully — every early exit is a failure #38

Description

@khaliqgant

Problem

A workflow cannot say "there is nothing to do here, stop, and that is fine." Every early exit is a step failure that fails the whole run.

Verified with a minimal workflow:

steps:
  - name: skipper
    type: deterministic
    command: "sh -c 'echo skipping; exit 78'"
  - name: after
    type: deterministic
    command: "echo AFTER_RAN"
    dependsOn: [skipper]
errorHandling:
  strategy: fail-fast
✗ skipper — FAILED: Command failed with exit code 78
○ after — skipped
[workflow] FAILED: Step "skipper" failed

There is also no conditional-step mechanism to route around it — the schema has no condition, when, skipIf, onlyIf, or if on any step type (checked all seven: WorkflowStep, AgentWorkflowStep, DeterministicWorkflowStep, WorktreeWorkflowStep, IntegrationWorkflowStep, WaitForWorkflowStep, CustomWorkflowStep).

Why it matters

"Nothing to do" is not an exception in a scheduled pipeline — it is the common case. Three examples from one real customer workflow, all of which are correct outcomes:

  • the brand is switched off behind a rollout flag
  • a circuit breaker has auto-disabled the brand after repeated failures
  • hourly cron, a manual trigger and a task redelivery raced for the same tick, and this one lost the claim

At ~3,000 brands ticking hourly, the third alone means a large and constant stream of runs that are supposed to stop early. Today every one of them is recorded as a failed run.

That inverts the signal. A dashboard that is permanently red teaches operators to stop reading it, and then a genuine failure is indistinguishable from the background. This customer's previous engine died silently for weeks for exactly that reason, so it is the specific failure mode the design is trying to avoid.

Current workaround, and why it is unsatisfying

The gate has to move outside the workflow: the caller runs a check first and only triggers a run for work that should proceed.

if node bin/check-breakers.mjs; then
  relayflows run workflows/autopilot-tick.yaml
fi   # exit 78 -> skip quietly, trigger nothing

This works and is arguably better architecture for a brand-level flag. It does not help the in-run cases. Losing a claim race can only be discovered inside the run — by then the workflow has started, and its only ways to stop are "fail" or "run the remaining steps anyway", which for agent steps means paying for model calls that should not happen.

Suggestions

Either would solve it; the first is smaller.

  1. A terminal-success exit. A reserved exit code, or a sentinel a step can emit, meaning end this run here, mark it succeeded, skip the rest. Downstream steps report skipped, the run reports completed.
  2. Conditional steps. A condition/when on a step, evaluated against prior step output, so a workflow can branch around work it does not need to do.

A distinct terminal state (completed_early / no_op) rather than plain completed would be even better — it keeps "ran and did work" separable from "correctly did nothing" without either being an error.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions