Skip to main content
In real systems calls fail, services go down and models return nonsense. A workflow engine’s quality shows in what it does next. Yukta’s position: failure is an ordinary event with a few well-defined outcomes, and the engine, not each plugin, owns them.

The engine owns failure, plugins do not

Retries, error routing and time limits are not features of a particular plugin. The engine applies them around every node, and you control them with a reserved engine object in the node’s configuration, which the plugin never sees. The benefit is consistency. An HTTP call, a script and a model call all retry the same way, time out the same way and route errors the same way. Plugin authors write business logic only. The cost is that the policy is per node and per message, so it is blunt: it cannot know that a 404 is final but a 503 is not unless you tell it which errors to retry.

Four things a failed message can become

When a step cannot handle a message, one of four things happens, and you choose which per node. Retry combines with the others: a message is retried, and only when it still fails is it failed, routed or dropped. A step reporting invalid input (a bad request, not a transient fault) is never retried, because retrying cannot help.

Why all-or-nothing output matters

With retry or error routing on, whatever an attempt emitted before it failed is held back and dropped. A retry or a dead letter therefore never lands on top of a half-finished result. You pay by buffering one message’s output until its attempt ends, which is a good trade for correctness.

Absorbing failure can hide a problem

Route and continue keep a flow alive. That is useful and dangerous: if a service is down, they will quietly shunt every message to a dead letter and the run will report success. A failure budget caps it. Once more than a set number of messages fail within a window, the node fails instead. Think of it as the difference between “a few bad records” and “something is broken”. Use route or continue together with a budget unless losing messages is truly fine.

How a failure spreads

A node that fails ends FAILURE. Nodes behind it end UPSTREAM_FAILED. The first terminal to fail ends the whole run and cancels the other branches. You can change this at a join. When a node has several parents, its join rule says what a failed parent means: This is how you build “send the report even if one optional check failed”. The consequence to keep in mind: a run can end SUCCESS with a failed branch inside it. Look at node statuses, not only the run status.

Failure is not only errors: time

A step that hangs is a failure too. Yukta bounds every run and node with clocks that you do not have to implement. See Time limits.

Design advice

  • Retry the transient, route the bad data, fail the broken. Most designs want all three on different steps.
  • Put side effects behind idempotence. A retried or resumed step can see a message twice. See Delivery and back-pressure.
  • Give errors somewhere to go. An error exit with no edge drops the message. Wire a dead-letter step so a failure is visible.
  • Do not retry AI calls blindly. A model that returned something wrong may return it again, and each retry costs tokens. Prefer a gate after the model, or a fallback to a different model. See AI and deterministic work.