Skip to content

then: how to chain a pipeline

Rules compose a progression from after blocks. Chapter 12 tells you to divide a large rule into small rules that react. But some work is truly a script: do this operation, then this operation, then complete. Nested after blocks hide a straight pipeline in the indentation. The then statement writes the pipeline from the top to the bottom. Below the surface, the statement desugars to the same nested after blocks.

A pipeline as one sequence

use std.script
use std.coercion

workflow Pipeline

output result Done
failure error Broken

class Done {
  priority string
}

class Broken {
  reason string
}

class Ticket {
  title string
}

table tickets as Ticket [
  {
    title "checkout is down"
  }
]

coerce judge(title string) -> Verdict """markdown
Classify: {{ title }}

{{ ctx.output_format }}
"""

class Verdict {
  priority "urgent" | "routine"
}

rule triage
  when Ticket as t
=> {
  then precheck <- exec "true"
  then verdict <- coerce judge(t.title)
  complete result { priority verdict.priority }
}

The then <binding> <- <effect> statement chains the success of the effect into the remainder of the block. Thus the exec statement runs first. The program requests the coercion only after the exec statement succeeds. The program gets to the terminal only after the coercion succeeds. Each then binding is the success payload of its step. The binding is in scope for each subsequent statement. Thus verdict.priority reads the typed result of the coercion two lines below the request. There is no nested after block.

The desugared form of a then statement

A then statement has no new semantics. It is only a different spelling. The rule above compiles to exactly the nested form that you can write manually:

rule triage
  when Ticket as t
=> {
  exec "true" as __then_precheck

  after __then_precheck succeeds as precheck {
    coerce judge(t.title) as __then_verdict

    after __then_verdict succeeds as verdict {
      complete result { priority verdict.priority }
    }
  }
}

The whip check command shows the desugared rule. Thus you can always examine the sequence. The __then_* names are the synthetic handles of the effects. The compiler reserves these names. If you write such a name, the result is a check error. The compiler hides these names deliberately. The then binding is the success payload. The binding is not the handle of the effect. If a step needs the handle, write the step in the traditional as and after form. A step that a cancel statement stops is an example.

Failure: the net and not a branch

A then statement chains success only. If a chained step fails or times out, the automatic failure net from chapter 6 catches the failure. The instance fails with the generic reason that gives the name of your binding. The reason is "unhandled failure of `verdict` in rule `triage`". This behavior is the deliberate contract of the sugar. A then chain states this: the failure of any step fails the run, and the generic reason is sufficient. This selection is explicit. Thus the whip check command gives no warning about an unhandled failure on a then step. The command gives such a warning for a bare effect that only a succeeds predicate handles.

When the failure of a step needs a typed reason or a recovery, write that step in the traditional form:

rule triage
  when Ticket as t
=> {
  coerce judge(t.title) as verdict

  after verdict succeeds as v {
    complete result { priority v.priority }
  }

  after verdict fails as f {
    fail error { reason f.reason }
  }
}

The two forms compose. A rule can chain its success path with then statements. The same rule can handle one sensitive step explicitly.

A then statement operates in each block

A then statement in an after block or in a case arm chains the remainder of that block:

after upload succeeds {
  then scanned <- exec "scan incoming.bin"
  record Clean { note scanned.stdout }
}

How to select between then and rules

Situation Use
Steps that always run in a set sequence and share bindings a then chain
A step whose failure needs a typed reason or a recovery as and after
Independent reactions to different facts separate rule declarations
One decision with more than one outcome a rule with a case statement
Many producers and one collector rules that facts connect

The boundary is clear. The then statement is for a pipeline. When the arms diverge, when the reactions become independent, or when different facts drive different responses, you describe rules. There is no flow construct. A chain deliberately has no branch. This is the statement of identity from chapter 1: case and pattern matching only.

Where next

Chapter 10 completes Part I with time. The chapter shows how to limit an effect with a timeout, how to use an absolute deadline, and how to run a workflow on a schedule.