Manual¶
This is the WhippleScript manual. It has one chapter for each feature of the language. Each chapter starts with the most simple form of its feature that operates. The chapter then adds more detail. Thus each chapter introduces the important behavior at the point where the behavior first becomes applicable. If you are a new user, start with the quickstart and the tutorials. The language reference is the short and complete specification. The chapters contain links into the language reference.
One premise applies to all of this manual. Rules hold the policy and stay deterministic. Each external operation is a durable effect. The runtime controls delivery, retries, idempotency, and inspection.
Chapters¶
Part I — Build workflows
| # | Chapter | Subject |
|---|---|---|
| 1 | The smallest workflow | the skeleton of a program; the check, run, and examine sequence |
| 2 | Facts and types | classes, enums, literal unions, optionals, tables, the identity of a fact |
| 3 | Expressions and assertions | conditions on facts; assert; queries |
| 4 | Rules | matches, guards, rewrites of facts, joins, the commit |
| 5 | Effects and durability | requests for external work; timers; the ledger; cancel |
| 6 | Error handling | deliberate failure; the failure of an effect; the automatic fail net |
| 7 | Branching with case |
branches on a closed set of data or outcomes |
| 8 | Prompts, coerce, and decide | prompt templates; typed model decisions; branches on a decision |
| 9 | then |
the sugar for a chain of steps; the desugar; the automatic fail contract |
| 10 | Time | timeouts, absolute deadlines, clock sources, @service |
Part II — Direct agents
| # | Chapter | Subject |
|---|---|---|
| 11 | Agents | agent, tell, turns, the three controls |
| 12 | Readiness patterns | the full vocabulary of when; the difference between after and when |
| 13 | Agent patterns | watchdogs, pipelines with a set rate, retries as facts |
Part III — Govern workflows
| # | Chapter | Subject |
|---|---|---|
| 14 | Coordination | leases, counters, ledgers; state that a workspace shares; contention as a branch |
| 15 | Trackers | backlogs, claim and finish, the whip issue commands, a person in the loop |
| 16 | Progressions | bindings as values; the during and until regions; lapse arms; whip progressions |
| 17 | Messaging & ingress | channels and send; the Message envelope; signals, peer signals, sources |
| 18 | Composition | bundles and input; invoke and milestones; include, pattern, and action |
| 19 | Exec: validation & hosted scripts | exec -> Schema; the hosted profile; manifests and pinned hashes |
| 20 | Files & file stores | stores with a root scope; the read-only default; read, write, import, and export |
| 21 | Checkpoints, restore & forking | cuts, restore that is honest about replay, forks of a conversation, the VCS substrate |
| 22 | Information flow I | envelopes and labels; sets of readers; the first denials; the guarantee report |
| 23 | Information flow II | egress through a provider; flow signatures; redact … keep; crossings with a mark |
| 24 | Governance envelopes & attestation | the split between two agents; signatures and attestation; hatches that permit an audit |
Part IV — Evaluate and optimize
| # | Chapter | Subject |
|---|---|---|
| 25 | Testing workflows | test blocks; given, stub, run, and expect; the virtual clock; the invalid-not-skipped rule |
| 26 | Gauges & evidence | dimensions of quality; the granularity of a judge; bars; ambient scores |
| 27 | Marks: pin, suppose & settle | cut points; retroactive pins; regeneration in pairs; certification |
| 28 | Campaigns & improve | declared trade partitions; the optimizer loop; the holdout set; adoption |
| 29 | Precedents, spend & estimators | durable decisions; caps that park work; the estimators; the reopener |
| 30 | Observability & tracing | the instrument panel; trace --check; lint; export to OTLP |
Part V — Extend WhippleScript
| # | Chapter | Subject |
|---|---|---|
| 31 | Authoring packages | the call effect; manifests, package check, and package lock; the permitted content of a package |
| 32 | Providers & harness configuration | the families of provider; configuration files; credentials as references; the seam |
| 33 | Script capabilities & the bash tool | grants for a script; the governed virtual bash tool; the design of a tool surface |
| 34 | Runtimes: native & the edge | the four loops; the native store; the placement of the Durable Object; deploy |
| 35 | Memory: recall, learn & curate | durable pools of memory in a workspace; the three verbs; whip memory; memory under governance |
Quick pattern guide¶
This section is the original single-page guidance for authors. It stays as a short reference. Each subject below also has a full chapter above.
Model data with classes and enums¶
Facts are typed. Use an enum for a closed set of decisions. Use a literal field for a small state machine:
enum ReviewStatus {
Accept
Revise
Blocked
}
class WorkItem {
id string
title string
status "queued" | "reviewed"
}
class WorkReview {
status ReviewStatus
reason string
confidence float
}
A status field with a literal type keeps the guards deterministic. It also
makes an illegal state impossible: a rule that matches
where item.status == "queued" cannot also see an item that has the reviewed
status.
Do not change a status in its position. Consume the fact and record a new fact
instead. The statement done item -> record WorkItem { ... status "reviewed" }
makes the change of state atomic with the other operations that the rule
commits.
Request agent work¶
agent worker {
provider fixture
profile "repo-writer"
capacity 1
capabilities ["agent.tell"]
}
rule implement
when WorkItem as item where item.status == "queued"
when worker is available
=> {
tell worker requires ["agent.tell"] as turn """markdown
Implement this item:
{{ item.title }}
"""
after turn succeeds as completed {
done item -> record WorkItem {
id item.id
title item.title
status "reviewed"
}
}
}
The tell statement records an effect. The provider runs the effect later.
Learn these three results:
- The sequence of the statements in a rule body does not set the sequence of
the effects. Use an
afterblock to make a dependency edge. - The output of an effect (
completedin the example) is visible only in theafterblock that proves the terminal status. This condition makes the causality auditable. - The
when worker is availableclause is the gate on capacity. Without this clause, the rule still fires. But the effect can then stop because of capacity.
Make typed model decisions with coerce¶
Sometimes a judgment must supply structured data and not prose. In this condition, declare a coerce function and branch on its completion:
coerce reviewWork(title string, summary string) -> WorkReview {
prompt """markdown
Review the completed work.
Title: {{ title }}
Summary: {{ summary }}
{{ ctx.output_format }}
"""
}
rule review
when ...
=> {
tell worker as turn "..."
after turn succeeds as completed {
coerce reviewWork(item.title, completed.summary) as review
}
after review succeeds as result {
record ReviewedWork {
item item
review result
}
}
after review fails as failure {
tell worker "Review failed: {{ failure.reason }}"
}
}
A coerce statement is an effect. It is not a call to a function. It is
durable, it can fail, and its typed output is available only in the after
branch.
Branch deterministically¶
A guard does the filter operation. A case statement covers a finite domain.
An after ... completes block does the exhaustive handling of the terminal
status:
case review.status {
Accept => {
record AcceptedWork { id item.id }
}
Revise => {
tell worker as revision "Revise {{ item.title }}."
}
Blocked => {
tell worker "Blocked: {{ review.reason }}"
}
}
after turn completes {
case turn.output {
Completed as result => { record TurnSucceeded { summary result.summary } }
Failed as failure => { record TurnFailed { reason failure.reason } }
TimedOut as timeout => { record TurnTimedOut { reason timeout.reason } }
Cancelled as c => { record TurnCancelled { reason c.reason } }
}
}
Branch on typed values. Never parse the text of a prompt to select a route or a
status. If a decision needs the judgment of a model, make the decision a
coerce statement and branch on the typed result.
Sequential chains with then¶
Independent rules are the best form for the largest part of orchestration. Each
rule reacts to the facts that apply to it. The runtime puts the rules in
sequence through after edges. But some work is truly a script: do this
operation, then this operation, then complete. Nested after blocks hide the
sequence of such work. The statement then <binding> <- <effect> writes the
sequence from the top to the bottom. The statement desugars to the same nested
after blocks:
rule triage
when Ticket as ticket
=> {
then plan <- tell triager "Plan {{ ticket.title }}."
then verdict <- coerce reviewPlan(ticket.title, plan.summary)
case verdict.choice {
"approve" => { complete result { plan plan.summary } }
"reject" => { fail error { reason "rejected" } }
}
}
Each then binding is the success payload of its step. The binding is in scope
for each subsequent statement. A then chain chains success only. If a step
fails or times out, the instance fails automatically. The generic reason gives
the name of the binding. This behavior is the explicit contract of the sugar.
If the failure of a step needs a typed reason or a recovery, write the step in
the traditional as and after form. The two forms compose.
Use this table to select between a then chain and plain rules:
| Situation | Use |
|---|---|
| Steps that always run in a set sequence, with shared bindings | a then chain |
| A step whose failure needs a typed reason or a recovery | as and after |
| Independent reactions that come together from different facts | separate rule declarations |
| Policy with many branches, where the data and not the position sets the sequence | rule declarations and guards |
The then statement is not a new mode of the runtime. It desugars to the
nested after form before the analysis. You can see the result in the
whip check output. Each property of a rule is still applicable: atomic
commits, durable effects, and the semantics of after.
Work queues¶
Sometimes work comes as a backlog and not as facts that you seed at the start.
In this condition, declare a tracker. The rules then claim from the tracker:
tracker backlog {
provider builtin
}
rule pick_up
when backlog has ready issue as issue
when worker is available
=> {
claim issue as work
tell worker as turn "Resolve {{ work.title }}."
after work fails as taken {
# another claimant won the race; wait for the next ready issue
}
}
The verbs are file issue into <tracker> { ... }, claim, release, and
finish. A claim statement that loses is a usual failure that you can
branch on. It is not an error. Thus a tracker with contention stays correct and
the source needs no lock.
Operate the backlog from the CLI with the whip issue new, whip issue list,
and whip issue show commands. The lifecycle verbs are ready, claim,
renew, release, and finish. The dep add command is also available. The
scope of the builtin tracker is the workspace. It gives identifiers in the
WS-1 format. An item that an agent files during a turn carries the provenance
of the run identity. The language reference
gives the full syntax.
Coordinate shared resources¶
Sometimes concurrent workflows contend for a resource that is limited. Examples
are a deploy slot, a budget of requests, and an audit trail. In this condition,
add use std.coord and declare one of the three coordination resources. Branch
on the atomic verbs. Do not build a lock from facts:
| Resource | Declaration | Verb | Outcomes |
|---|---|---|---|
| Lease (a mutex or semaphore with a limit) | lease name { key Type slots N ttl 10m } |
acquire <lease> for <key> [until ttl] as x / release x / renew x [until <dur>] as r |
held / contended |
| Ledger (a log that permits append only) | ledger name { entry Schema partition by <field> retain 30d } |
append <Schema> { ... } to <ledger> as x |
appended |
| Counter (a budget with a cap) | counter name { key Type cap N reset hourly\|daily\|weekly\|monthly [timezone "<IANA zone>"] } |
consume <counter> for <key> amount <n> as x |
ok / over |
Each attempt is atomic. Each outcome is a branch and never an error. Handle the two sides. If you do not, the checker flags the rule:
use std.coord
lease deploy_slot {
key Ticket
slots 1
ttl 10m
}
rule ship
when Ticket as t
=> {
acquire deploy_slot for t.id until ttl as slot
after slot held {
release slot
complete result { note "shipped" }
}
after slot contended {
fail error { reason "another deploy holds the slot" }
}
}
The safety limits are structural. Each lease has a TTL limit. Each ledger has a
retention limit. Each counter has a cap and a periodic reset. Declare the
timezone value. If you do not, the boundary uses UTC and the checker gives a
warning. You must release a held lease on the held branch or get to a
terminal. If the progression ends in a different manner, the runtime releases
the lease automatically. A cancel operation is one such manner. Thus a holder
that stops cannot block the slot. A hold with until ttl expires without help.
The release verb operates on two resources. A release of an acquire binding
frees the lease. A release of a claimed work item returns the item to the
tracker.
By default, a resource is private to the program that declares it. Add shared
to the declaration block to coordinate across the workflows in the same
workspace. The scope of the coordination state is the workspace. The
coordination state continues after a run store is deleted. The coordination
state is in .whipplescript/coordination.sqlite, or in the path that
WHIPPLESCRIPT_COORDINATION_STORE gives. Examine the state from the CLI:
whip [--json] leases [<resource>]
whip [--json] ledger [<ledger>] [--partition <value>]
whip [--json] counters [<counter>]
Admission is real. The effects run with the lease.acquire, lease.release,
ledger.append, and counter.consume capabilities. The embedded std.coord
package registers these capabilities when the store initializes. Thus an
operator does no setup. But an operator profile can make these capabilities
smaller, as with any other capability. The
language reference gives the full syntax. The
API reference gives the CLI detail.
Time and deadlines¶
Keep time out of a guard. A guard must stay pure. Write a deadline as an effect:
- The
timeout <dur>clause on an effect limits the permitted duration of the effect. Anafter ... times outbranch or anon timeoutbranch reacts when the limit expires. - The
timer <dur> as deadlinestatement is a delay that is independent. You branch on the delay withafter deadline succeeds. - The
cancel <binding>statement stops an effect that is pending or in operation. You must bind the effect before you cancel it.
tell worker as turn timeout 10m "Do the work."
after turn times out as t {
tell worker "Worker exceeded 10m — escalate?"
}
A duration has the form <n><unit>. The units are s, m, h, and d. In
the native worker on your machine, a timer or a timeout fires on a worker pass.
There is no daemon. Thus the whip run --until idle command holds a pending
timer to be idle. The whip status command lists the time effects that an
instance waits for. The Durable Object runtime in the cloud is different. It
fires a timer without help, through a DO alarm. Refer to
Runtime & operations.
Express retries as facts¶
The language has no built-in retry policy. A retry is a usual fact and a usual rule. Thus a retry stays visible and auditable:
rule attempt_job
when Job as job where job.status == "pending" and job.attempts < 3
when worker is available
=> {
tell worker as turn "Do job {{ job.id }}"
after turn succeeds as ok {
done job -> record JobDone { id job.id }
}
after turn fails as failed {
done job -> record Job {
id job.id
attempts job.attempts + 1
status "pending"
}
}
}
rule give_up
when Job as job where job.attempts >= 3
=> {
done job -> record JobAbandoned { id job.id }
}
Gate on humans¶
A review by a person uses the tracker. File an issue with
file issue into <queue>. A person then works the issue with the whip issue
command. The person can list, claim, comment, and finish. A rule reacts to the
finish of the issue. The rule can use a when <queue> has ready issue as x
clause, or the rule can match the finish. Thus the work of a person is in the
same durable record as the other work. You can examine the record. You do not
operate a separate inbox for questions.
Run a local command (an escape hatch)¶
The exec statement has a dev form and a hosted form.
The exec "<command>" as result statement runs a local command as an effect.
The statement makes result.exit_code and result.stdout available. This
statement is an escape hatch for the dev profile. It has deliberate limits:
- The program must import
std.script. The source syntax cannot grant a command. The operator makes an allow-list of commands through theWHIPPLESCRIPT_EXEC_ALLOWvariable. The value is a list of glob prefixes such asscripts/*, with a colon between each prefix. If the import is absent, or if there is no allow-list, admission blocks the effect. The block reasons areblocked_by_capabilityandsecurity.script_disabled. The effect then never runs. If the list is not empty and a command is not in the list, the effect fails and goes to theafter x failsbranch. - A raw dev
execstatement has no sandbox. A grant is a decision to trust, and the record shows the decision. Keep the allow-list as small as the workflow needs. If an agent, a package capability, or a child workflow is applicable, use it in place ofexec.
In a hosted deployment, use a named script capability instead:
The operator supplies --exec-profile hosted --script-manifest <path>. The
manifest maps backup_repo to an argv list, a pinned SHA-256 digest, and
optional references to secrets:
{
"backup_repo": {
"argv": ["bash", "scripts/backup.sh"],
"sha256": "9f2c...",
"env": { "BACKUP_TOKEN": "env:BACKUP_TOKEN" }
}
}
A hosted exec statement rejects a raw command string. It verifies the bytes
of the script before it starts the process. It runs the argv list directly with
typed JSON on stdin. It records the hash of the script that ran.
Use exec for a deterministic local step that belongs in the workflow. A test
script and a linter are examples. Do not use exec to move orchestration into
the shell.
Compose the source¶
| Need | Use |
|---|---|
| Divide the declarations across files | include "schemas/common.whip" |
| Bring in coerce classes and functions | include "review.coerce" |
| Import the surface of a package or a library | use std.memory |
| Reuse a fragment of a rule or an effect at compile time | pattern and apply |
| Put fixed steps in sequence with shared bindings | a then chain |
| Take work from a durable backlog | tracker and claim |
| Run work with its own lifecycle and terminal contract | workflow and invoke |
A pattern is a template at compile time. The apply statement expands the
pattern into usual declarations before the type check. The invoke statement
is composition at run time. The child is a true instance. The parent sees only
the declared output payload or the declared failure payload of the child:
invoke ReviewPhase {
phase PhaseReviewRequest {
id phase.id
title phase.title
}
} as child
after child succeeds as result {
record ReviewComplete { phaseId phase.id result result }
}
after child fails as failure {
record ReviewBlocked { phaseId phase.id reason failure.reason }
}
End the workflow¶
Declare the data that the workflow supplies. Then make sure that one rule supplies that data:
The complete result { ... } statement and the fail error { ... } statement
are atomic with the commit of the rule. The runtime validates the payload
against the declared contract. The whip check command rejects a workflow that
has no path to a terminal. If a workflow runs continuously by design, add the
@service tag. If a rule gets its data from an external source, add the
@external tag. Refer to
liveness checks.
Remember the division between two types of failure. A provider failure is state
of an effect or a run. A rule reacts to that state. A fail statement is the
workflow itself that stops. Do not mix the two types. The source must express
the policy that decides which provider failures are fatal.
Debug a run¶
Use these views in sequence:
whip status <instance> # lifecycle, counts, recent events
whip log <instance> # the event sequence
whip facts <instance> # current fact state
whip effects <instance> # effect status + policy_block_reason
whip runs <instance> # provider attempts
whip diagnostics <instance> # recorded errors
whip evidence instance <id> # provider payloads and artifacts
whip evidence [<gauge>] # gauge evidence view (estimates + contradiction flags)
whip trace <instance> --check # lifecycle conformance
The tightest authoring loop is the command
whip --json run <file> --provider fixture --until idle together with
assertions. An assertion changes "the workflow appears to operate" into a
checked statement about the final state.
Checklist before you supply a workflow to other persons¶
- The
whip checkcommand passes. The@servicetag and the@externaltag are present only where you intend them. - The
afterblocks and not the sequence of the source set the sequence of the effects. - Each routing decision uses typed data in the source: an
AgentRefvalue, an enum, or a literal. No routing decision uses the output of a model. - Each failure branch does a retry, an escalation, or a deliberate no-operation. No failure branch is silent.
- Each agent profile is as small as the work permits. Each package call is an
explicit
calleffect. You attach a skill to an agent or to a turn. You do not import a skill. - The
whip --json run --until idlecommand passes its assertions with the fixture provider. Thewhip trace --checkcommand reports conformance.
Usual mistakes¶
| Mistake | Correction |
|---|---|
| You use the sequence of the source to sequence the effects. | Use after effect succeeds, after effect fails, or after effect completes. |
You read the output of an effect outside its after branch. |
Bind the output in the branch that proves the status. |
You use coerce as a call to a local function. |
Branch on the completion of the effect. |
| You let a model select the provider or the route. | Use AgentRef<...>, an enum, or a literal field. |
| You treat a provider failure as a workflow failure. | Write a rule that decides when to use fail. |
You import a skill with use. |
Attach a skill to an agent or to a turn. The use statement imports the surface of a package or a library. |
| You hide orchestration in a shell script around the CLI. | Write the orchestration as rules, facts, and effects. |
| You read the clock in a guard. | Use a timeout, a timer, or a recorded fact. |
You treat a lost claim as an error. |
Branch on the failure of the claim and wait for the next ready item. |
You use a bare emit <name> statement to log an event. |
The bare emit statement is removed. The emit signal and emit milestone statements stay. Derive a fact from the completion of an effect. |
You grant a raw dev exec statement broadly. |
Keep WHIPPLESCRIPT_EXEC_ALLOW as small as the workflow needs. For authoring that you do not trust, use a hosted script capability. |
You put credentials in the .whip source. |
Use a reference to the provider configuration. |