Your first decision gate
Walk through Syncropel's self-improvement loop. Emit a record that has no routing rule yet, see the engine propose one as a decision request, approve it from the decision gate, and watch the next similar record route automatically.
What you'll learn
In about 30 minutes, you'll exercise the decision gate (ADR-139) — the path that makes Syncropel a learning system, not just a record store. You'll emit a record the engine doesn't yet know how to route, see it surface a decision request for your approval, approve it, and verify the next similar record routes without any new reasoning.
After this, you'll understand why Syncropel is described as "self-improving" — the engine never silently changes routing or policy. It proposes, an operator decides, and the decision becomes a record that future runs replay.
"AITL" is retired. Earlier releases called this loop "AITL" (Actor-In-The-Loop) and shipped an spl aitl command. As of v0.107 the mechanism is the decision gate: a proposal is a core.work.decision_request.v1 record, and you answer it with spl decisions. The concept is unchanged — the engine proposes, a human decides, the decision is a durable record — only the surface is cleaner and now covers any actor, not just the intelligence loop.
Before you start
spl 0.107.0or later (spl --version)- A running instance — either local (
spl serve) or hosted (<label>.syncropel.app). spl statusreturnshealthy.- An LLM provider key configured (
spl config set-key anthropic <key>) — the engine's intelligence loop needs a model to reason about new routing. On a hosted instance the key is already wired.
If you've never emitted a record before, do Your first thread first. This tutorial assumes you can read spl thread records <id> output.
1. Confirm intelligence is enabled
Intelligence (the loop that reasons about unrouted work and proposes decisions) ships off-by-default on local installs. Check:
spl config show | grep -i intelligenceIf you see intelligence_enabled: false (or no entry at all), enable it by giving the engine a model to think with:
spl config set-key anthropic <your-key>
spl config model claude-sonnet-4-6The engine hot-reloads on the next config broadcast — no restart.
2. Emit a record with no matching routing rule
Pick a body kind the instance hasn't seen. The simplest is a fresh thread with a do-something intent:
spl intend "Translate the README.md to French" --thread th_fr_translateOutput includes a record id and thread id:
✓ Record created
thread: th_fr_translate
record: rec_8a3c...
act: INTENDThe engine ingests it, runs the routing match cascade, and finds nothing — no L0/L1/L2/L3 hash hit, no semantic match, no static rule. That miss is what triggers the intelligence loop.
3. See the proposal land in the decision queue
Within a few seconds, the intelligence loop reasons about the unrouted intent. Because it isn't confident enough to auto-apply (confidence ≥ 0.90 auto-applies; ≥ 0.60 asks a human; < 0.60 is logged and dropped), it emits a decision request and waits for you. List the pending decisions:
spl decisions listYou'll see something like:
◈ 72% Intelligence proposal (confidence 72%): Add a new routing rule
that matches INTENDs like "translate <doc> to <lang>" and routes
them to the translator agent.
id: e6a0f87dfc917aba...
spl decisions approve e6a0f87dfc917aba
1 pendingThe request carries a structured body: the match expression the engine synthesized, the recommended target actor, and a confidence score. The full body is visible with:
spl decisions list --json | jq '.[0]'Until you act, no rule exists in th_engine_config and no future record will route differently. The same pending decision also shows up in spl resume under NEEDS — the decision gate is the one queue for "anything on the instance is waiting on you."
4. Approve the proposal
spl decisions approve is sugar for the CHOOSE response choose <id> approve — approving a routing proposal tells the engine to apply it:
spl decisions approve e6a0f87dfc917abaOutput:
✓ Decision answered
request: e6a0f87dfc917aba...
response: approve
result: routing_rule_added (name: routing_translator_v1)What happened mechanically: your approval is itself a record — a DO with body.fulfills pointing at the core.work.decision_request.v1. The engine reads the answered request, recognizes it as a routing-rule proposal, and emits a routing_rule.add.v1 LEARN on th_engine_config. The engine hot-reloads the rule on broadcast. From this point forward the rule is part of the instance's state — not config-file state, not in-memory state, record state that survives restarts and round-trips through spl export / spl import.
Verify the rule landed:
spl config list-rulesYou should see routing_translator_v1 in the list.
Three ways to answer a decision. Not every decision is yes/no. A core.work.decision_request.v1 declares one of three response shapes, and the CLI has a verb for each:
- CHOOSE — pick from declared options.
spl decisions choose <id> <option-id>;approve/rejectare sugar for the common two-option case. - PROVIDE — supply an open value.
spl decisions provide <id> --value <json-or-string>. - ACKNOWLEDGE — a zero-data "seen it."
spl decisions ack <id>.
Same gate, same audit trail, whether the asker is the intelligence loop, a paused work loop, a spl decompose plan, or a federated peer.
5. Emit a similar record and watch it route automatically
spl intend "Translate the CONTRIBUTING.md to Spanish" --thread th_es_translateThis time the routing match cascade hits at L1 (or L2, depending on how the proposal generalized) and the engine fires an INTEND at the translator agent without any new reasoning. Inspect the thread:
spl thread records th_es_translateYou should see two records back-to-back: your INTEND, and a follow-on engine-emitted DO with body.dispatch_handled: true showing the route fired.
Compare to step 2's thread, which has only your bare INTEND with no follow-on — that's the difference between "the engine is reasoning every time" and "the engine is replaying a learned rule."
6. Inspect the audit trail
The proposal, your answer, and the rule application are all records. The decision request opened on its own thread; the answer fulfills it; the rule application lands on th_engine_config. Export the security-relevant slice — spl audit export emits one JSON record per line — and filter it:
spl audit export --since 1h | jq 'select(.body.kind | test("decision|routing_rule"))'Every decision is a record. There is no "approved by gut feel" path — the trail is structurally complete (per the governance concept), and the same trail is what trust scoring uses to attribute future routing successes to your approval.
What just happened
You exercised Syncropel's decision-record loop:
- A miss is data. When routing finds no match, the engine doesn't fall back to a hardcoded default — it emits a structured
core.work.decision_request.v1and waits. - An operator's answer is a record. Not a flag, not a session decision — a DO that fulfills the request, which any future replay (export → import, fold rebuild, audit) reads as canonical.
- The rule is record-derived, not config-derived. Routing rules live on
th_engine_configas LEARN records. The fold over that thread is the engine's runtime config. State = fold(records). - Replay closes the loop. The next similar record matches and skips reasoning entirely. Cost goes from "one LLM call" to "one expression evaluation" — that's the gradient that makes the system economically viable at scale (see patterns).
This is the smallest example of why the decision gate is Syncropel's self-improvement primitive: the engine proposes, you decide, and your decision becomes a durable record.
Where to next
- Your first crystallized pattern — what happens after the same approved rule fires 100 times: the pattern crystallizes, replay short-circuits the cascade entirely.
- Patterns concept — the four hash levels, crystallization criteria C1-C4, the matching cascade.
- Engine concept — the four loops (INGEST, RECONCILE, TICK, CRON) and how the decision gate slots into RECONCILE.
- Governance concept — why permissions, the decision gate, and audit are all the same primitive (records on reserved threads).
- CEL expressions guide — author your own routing rules directly instead of waiting for the engine to propose them.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
spl decisions list always returns empty | Intelligence is disabled, so the engine never proposed anything. | Confirm spl config show reports a model + provider key. Enable with spl config set-key <provider> <key> and spl config model <name>. The instance hot-reloads on the next broadcast. |
| A decision stays pending forever | You're reading from a paired peer, not the originating instance. Decision state is per-instance until the resulting rule federates. | Run spl decisions list against the instance that emitted the original record. Check spl actor list to confirm which DID is the active actor. |
spl decisions approve returns 403 forbidden | Default-secure auth on a hosted instance — answering a decision emits a record, and the bearer token lacks write scope. | Mint a service account with write scope: spl service-account create --name "Decider" --scopes records:write --with-token, then spl token save <token>. |
| Approved rule didn't fire on the next similar record | The proposal generalized too narrowly (matches L0 only) — the second record differs at L0. | Inspect with spl config list-rules, find the rule, and broaden its CEL match expression. The pattern layer will eventually crystallize the broader shape on its own; manual editing is the fast path. |
spl audit shows no decision records after approving | The approve command ran against a different namespace from the one you're reading. | Pass --namespace <ns> to match the namespace the original record was emitted into. |
Your first federation pair
Pair two instances in one command. Emit on one, query on the other, see federated records flow consent-gated. The smallest two-instance walkthrough.
Your first crystallized pattern
Do the same thing three times, watch the pattern detector form a hash chain, see the trust score evolve, and observe automatic routing kick in. Syncropel's cost curve bends here.