FlowDrop Workflow Specification 1.0-draft

A refused signal says why in a code a client can act on

Three different refusals share one status code, and a client has to tell them apart to say anything useful to an operator. The discriminator is a stable code, not the wording of a message.

The rule

Normative: this is the rule
  1. The signal API refuses in three distinguishable ways, all 409, each carrying a stable machine-readable code beside its human-readable message: the run is in a terminal state and cannot be signalled or resumed (PIPELINE_TERMINAL); an inward signal is already pending for the run (INWARD_SIGNAL_ALREADY_PENDING); and there is no active pause to resume (NO_ACTIVE_PAUSE).
  2. A client classifies a refusal by that code; the message wording is not a contract and an implementation may change it.
  3. A refusal answers with the error envelope (API-1).
  4. A newly created cancel or pause signal answers 202; a resume that resolved a pending pause answers 200.
  5. A signal addressed to a run that does not exist answers 404 to a caller holding blanket authority over runs (existence is no secret from someone who may act on any run), and an opaque 403 to every other caller, indistinguishable from the answer for a run that exists but is not theirs, so that the route cannot be used to enumerate which runs exist.

What it means

Three different refusals on the signal API share the one status, 409, so the status alone cannot tell an operator which of the three happened; the error_code is what does. A code, once published, keeps its meaning forever — API-8 covers that guarantee for two of the three codes here; the third, PIPELINE_TERMINAL, is the same contract applied to a run that has already finished.

Success is not one code either: creating a new signal and resolving one that already existed are different events on the same door, and they answer differently so a caller can tell which happened without inspecting the body.

The existence check on a missing run is scoped to who is asking. A caller who may already act on any run loses nothing by being told a run does not exist, so they get an honest 404. Everyone else gets the same opaque 403 whether the run does not exist or simply is not theirs — the two cases must be indistinguishable, or the difference between them becomes a way to enumerate which runs exist.

Example

Cancelling a run that has already finished409 — PIPELINE_TERMINAL
POST /flowdrop/api/pipelines/{pipeline}/cancel

{}
Pausing a run for the first time202 — accepted
POST /flowdrop/api/pipelines/{pipeline}/pause

{}
Resuming that same pause200 — resolved
POST /flowdrop/api/pipelines/{pipeline}/resume

{}

A caller with blanket authority over runs who cancels one that does not exist gets 404; every other caller gets 403 for that same request, and 403 again for a run that exists but is someone else's — the two answers are identical on purpose.

Why

Recorded under OPEN-18.

Rule identifiers are permanent and are never renumbered. Each implementation publishes its own standing against these rules; this specification does not.spec 1.0-draft · INT-16 · changed in spec 1.0