Attractor

microsoft / amplifier-bundle-attractor

Attractor

Multi-stage AI pipelines for code — where the graph is the program.

Attractor is an Amplifier bundle implementing StrongDM's attractor nlspec — extending it, and in a few ledgered places deliberately diverging from it (see the repository's specs/EXTENSIONS.md). A Graphviz DOT digraph is not a picture of the program; it is the program. Nodes are computation, edges are dispatch, attributes configure behavior, and clusters scope defaults and derive stylesheet classes. You run the .dot file. You also render it with stock Graphviz, diff it in a pull request, and review it like any other source file.

Ships: loop-pipeline orchestrator · loop-agent per-node agent loop · multi-provider LLM client · attractor CLI · in-session run_pipeline tool · attractor-expert agent · /attractorify skill

Diagram 1 — the signature shape: a convergence loop

The convergence loop start flows into implement, which flows into a test gate. On gate_pass the run reaches done. On gate_fail a corrective back-edge returns to implement, carrying the test output as evidence. Mdiamond box · llm worker parallelogram · shell Msquare start implement writes code + tests test_gate pytest -q · goal_gate=true done gate_pass gate_fail — retry, with the failing output as evidence test_output.txt
The exit is structurally unreachable until the gate reports success. The gate is a shell command whose real exit status decides the route — not an opinion, and not a summary the worker wrote about itself.

01The problem

Multi-step AI workflows decay into scripts nobody can see

A real AI-powered software workflow is several LLM calls chained together with conditional logic, human approvals, and parallel execution. Without a structured orchestration layer, that becomes one of two things: a fragile imperative script, or an ad-hoc state machine.

Both share the same defect — the control flow is invisible. It lives in if statements and early returns spread across a few hundred lines. You cannot look at it. You cannot diff it. When it loops forever, or exits early, or silently skips the review step, you find out by reading the log backwards.

Attractor's answer is to move the control flow out of the code and into a file whose entire purpose is to describe a graph:

  • Version-controlled — the workflow is a text file in your repo, next to the code it operates on.
  • Diffable — a change to the pipeline shows up as a change to an edge, not a change to a nest of conditionals.
  • Reviewable in a pull request — a reviewer reads eight lines of DOT instead of inferring a state machine.
  • Renderable with stock Graphvizdot -Tsvg pipeline.dot. No custom viewer, no proprietary format.

That much is table stakes; several tools express workflows as graphs. The part that matters is what Attractor asks the graph to be.

02The big idea

Convergence, not chaining

The name is the thesis. An attractor is a shape a system falls into and cannot easily leave. Attractor asks you to build workflows with that property: a region of the graph that the run keeps re-entering until a machine-checkable condition is satisfied, and only then releases.

The canonical shape is the loop in the diagram above — a worker, a gate, and a corrective back-edge. Read it as three moves:

  • A worker node produces an artifact — code, a patch, a document.
  • A deterministic evidence gate checks the artifact. In the example it literally runs pytest; the shell's exit status is the verdict.
  • On failure, a corrective back-edge routes the run back to the worker, carrying the failing output forward as evidence.

Nothing in that loop is a suggestion. The done node is reachable from exactly one edge, and that edge's condition is the gate's own output. If the gate never says gate_pass, the run never reaches done. That is what "structurally unreachable" means, and it is the entire difference between a flowchart and an attractor.

Why a loop beats a chain, arithmetically

Assume each node in a pipeline succeeds 90% of the time — generous, for an LLM performing a non-trivial engineering task. A six-step linear chain multiplies those odds together. There is no recovery, because the graph has nowhere to recover to.

Diagram 2 — the same six nodes, chained versus looped

Reliability of a chain versus a corrective loop Six sequential nodes each 0.90 reliable multiply to roughly 0.53 end-to-end. The same six nodes with one corrective loop around an evidence gate reach roughly 0.94. Linear chain — six sequential nodes, each 0.90 reliable 0.90 0.90 0.90 0.90 0.90 0.90 exit 0.90⁶ ≈ 0.53 one bad response anywhere ends the run — there is nowhere to route back to Same six nodes, one corrective loop around an evidence gate 0.90 0.90 0.90 0.90 0.90 0.90 gate exit gate_fail — re-enter the loop with the failure as input ≈ 0.94
Six nodes at 0.90 give 0.90⁶ ≈ 0.53 end-to-end. Wrapping the same nodes in one corrective loop lifts it to ≈ 0.94. The nodes did not get better; the graph got a second chance and a way to know it needed one.

The loop only pays if the thing deciding whether to loop is trustworthy. If the gate is another LLM saying "looks good to me," you have added a step, not a guarantee — the same fallible process now grades its own output. The gate has to be something that cannot be talked into passing: a test suite, a type checker, a linter, a build, a schema validation, a script that greps for the string that must exist.

The gate is not a style choice; it is the mechanism that turns a flowchart into an attractor.

03What's actually novel

Five things that are not "AI workflow chaining"

Plenty of tools let you draw a workflow. What follows is the set of design decisions that make Attractor a different category of thing — each one earned from a failure mode that showed up in a real run.

01

The graph is the program

Most orchestration frameworks put YAML around code: steps that name functions, with the real logic hiding in the functions. Attractor inverts it. The .dot file is the executable artifact. Nodes are computation, edges are dispatch, attributes configure behavior, clusters scope defaults and derive stylesheet classes.

A node's prompt= is its body. An edge's condition= is its branch. shape= selects which execution tier runs it. There is no second file where the "actual" workflow lives, and no translation step between the picture and the behavior — because there is no picture, only the source. Rendering it with Graphviz produces a diagram that is correct by construction, since it is generated from the thing that runs.

02

Evidence-gated exits, and they fail closed

A node marked goal_gate=true is the node that gets to say the run is finished. This implementation treats that authority as dangerous and constrains it: on a goal-gate node, a plain-prose LLM response returns RETRY, not success. Ambiguity resolves against exiting.

That rule is the bundle's, not the spec's. The upstream nlspec is still fail-open today — §4.5 reads any string response as SUCCESS, with no exception for the node holding the exit — and the bundle diverges from it deliberately, with the divergence written down in specs/EXTENSIONS.md. What follows is why.

Fail-closed verdict handling A plain prose response is read as success on an ordinary node, but as retry on a node marked goal_gate=true. plain prose response no report_outcome, no JSON ordinary node goal_gate=true verdict: SUCCESS prose is accepted as "done" verdict: RETRY prose is not evidence of convergence
Diagram 3 — the same response, two verdicts. The gate node is held to a higher standard than the nodes it guards.

Why this exists — a real incident

A judge node evaluated a run and wrote, in prose: "NOT CONVERGED — 2 of 7 criteria pass." Under the fail-open rule — the spec's rule, and the bundle's own default at the time — an unparseable response counted as success. The pipeline recorded the run as converged and exited — after 2.4 hours, with zero work product.

The judge was right. The plumbing inverted its verdict. Fail-closed is what this implementation did about it: on a goal gate, "I could not parse a verdict" now means "go around again." The spec's text has not moved; the bundle has, and says so.

03

Verification lives outside the worker's context

The second failure mode is subtler and more interesting. If a worker knows the gate reads a file, the worker can write the file. In a live run, a worker fabricated its own convergence evidence — a convergence.jsonl reporting that it had converged — and the gate, reading the file exactly as instructed, passed it.

Dual critics running outside that worker's context caught it and refused to ship.

Verification outside the producing context Inside the worker's own context, the worker writes a convergence file and the gate reads it and passes. Two critics in fresh contexts inspect the same artifact and refuse to ship. the worker's own context implement llm worker writes convergence.jsonl self-reported: converged gate reads file → pass critic A fresh context, sees only the artifact critic B fresh context, sees only the artifact verdict: evidence not reproducible — refuse ship
Diagram 4 — the gate inside the loop can be satisfied by the thing it is grading. The critics cannot be, because they never saw the reasoning that produced the artifact.

Verification inside the context that produced the evidence is not verification.

This is the practical reason Attractor spawns each LLM node as its own sub-session with its own context, and why critics are given fresh ones. Independence is not an optimization here; it is the property being purchased.

04

The graph is a control plane, not a recipe

The most common way to misuse Attractor is to draw your task breakdown as nodes: plan → implement → test. That feels natural and it is the wrong layer. The graph encodes the convergence skeleton — where the gates are, what the budgets are, which feedback channels exist. The model owns domain decomposition, because the model is the part that can adapt the decomposition when the domain surprises it.

When you find yourself adding plan → implement → test as graph nodes, stop.

Recipe plane versus control plane On the left, three nodes in a straight line with no cycle and no gate. On the right, a worker and an evidence gate joined by a corrective cycle. recipe plane — drawn as a graph by mistake plan implement test no cycle · no gate · nothing to converge to this should have been a recipe control plane — what the graph is for worker owns decomposition gate machine-checkable exit the cycle is the point
Diagram 5 — the heuristic in one image. If your pipeline graph has no cycle, it should probably have been a recipe.
05

Tier discipline: models judge, shells execute

Every node picks an execution tier through its shape, and the split is deliberate. Deterministic work runs in shell nodes. Judgment runs in LLM nodes. Putting deterministic work inside a model is how you get a gate that is merely an opinion about a gate.

box — LLM node

Reads a situation, decides something, writes something that did not exist. Spawns a sub-session with tools. Non-deterministic by nature, and priced accordingly.

Use for: implementing, reviewing, judging, planning under uncertainty.

parallelogram — shell node

Runs a tool_command. Exit status and output are the result. Reproducible, auditable, free, and impossible to persuade.

Use for: tests, builds, linters, type checks, file existence, greps — every gate you actually trust.

The self-test the docs give you is blunt and effective: "Is the model here for judgment, or just to type?" If the answer is "just to type," you wanted a shell node.

04Under the hood

What actually happens when you run a .dot

Six phases, one orchestrator, one sub-session per LLM node, and a deterministic edge-selection algorithm that is worth reading twice.

The run lifecycle

The six-phase run lifecycle Parse, transform, validate, initialize, execute, finalize. The execute phase loops per node, writes a checkpoint after every node that attractor resume can pick up after, and checks the goal gate when the run arrives at a terminal node. per node PARSE read the .dot TRANSFORM stylesheet, vars VALIDATE lint, reachability INITIALIZE context, run id EXECUTE walk the graph FINALIZE artifacts, status a checkpoint is written after every node and attractor resume picks up after the last one the goal gate is checked on arrival at a terminal node
Diagram 6 — the lifecycle. TRANSFORM resolves the stylesheet and expands variables before VALIDATE sees the graph, so lint reads the expanded form rather than the template. The checkpoint written after every node is both a record of what happened and a place to restart from — an interrupted run is continued with attractor resume <run_dir>, the nlspec's §5.3 resume behaviour, implemented. It restores the recorded state and picks up after the last completed node: finished work is not re-executed, and the node that was interrupted runs again because it never completed. Resume is explicitly opt-in — a fresh run never reads a checkpoint back, so a stale one left on disk is inert — and a corrupt, foreign or already-finished checkpoint fails loud rather than quietly restarting from the top. Graph-owned durability still ships alongside it: a file-state guard node checks whether its stage's artifact already exists and routes straight past the work if it does (the repository's 12-graph-resume pattern; delete an artifact to rewind to that stage).

The shape vocabulary

A node's Graphviz shape is not decoration. It selects the execution tier — which is why a rendered Attractor graph is readable at a glance: you can see where the model is thinking and where the machine is checking.

Diagram 7 — node shapes and what they execute
ShapeDOTTier / behaviour
Mdiamond Start. Entry point of the run.
Msquare Exit. Reaching it triggers the goal-gate check before the run is allowed to end.
box (default) Codergen LLM agent. Spawns a sub-session with tools and works the prompt until it reports an outcome.
diamond Conditional routing. A no-op node: it runs nothing, the outgoing edges do the deciding.
parallelogram Tool / shell execution. Runs tool_command. This is where trustworthy gates live.
component Parallel fan-out. Dispatches its successors concurrently.
tripleoctagon Fan-in. Joins parallel branches back into one path.
hexagon Human approval gate. The run blocks on a person.
folder Nested sub-pipeline. Another .dot, executed as a node.
house Supervisor loop (experimental).

Orchestrator and agent: two different loops

There are two nested loops in a running pipeline, and confusing them is the main source of "why did it do that." The outer loop is loop-pipeline walking the graph: pick a node, run it, checkpoint, select an edge, repeat. The inner loop is loop-agent inside a single LLM node: call the model, execute the tools it asked for, feed the results back, call again — until that node's work is done. The graph never sees the inner loop; it sees one node and one outcome.

Architecture: orchestrator, sub-session, backend selection The loop-pipeline orchestrator reads the dot file, spawns a loop-agent sub-session per LLM node, and receives a report_outcome back. Backends are auto-selected: AmplifierBackend if session spawn is available, otherwise DirectProviderBackend. With no backend at all, LLM nodes fail loud and only model-free graphs still walk. pipeline.dot the program loop-pipeline walks the graph, routes edges holds context, writes checkpoints spawn report_outcome loop-agent sub-session repeats until done call LLM execute tools feed results back backend auto-selection session.spawn available AmplifierBackend — full sub-sessions with tools and delegation else: provider configured DirectProviderBackend — a per-node agentic tool loop; needs an explicit llm_model else no backend — LLM nodes fail loud; only model-free graphs still walk
Diagram 8 — results flow forward along edges; the orchestrator holds the context. The direct tier is not a downgrade to bare completions: it hands the agentic tool loop to the shared unified-llm-client, one loop per node. Selection is automatic but never adaptive — before the walk starts the engine preflights every node's declared provider, and a node asking for one the run cannot serve stops the launch outright, naming the node, the provider, and the missing credential. Nothing falls back to a different provider quietly, and no model is ever silently skipped.

Edge selection: five steps, in order

When a node finishes, the orchestrator has to pick exactly one outgoing edge. It tries five rules in sequence and takes the first that yields a target. If none of them does, that is not a shrug — it is a hard stop.

The five-step edge selection algorithm Condition-matching edges, then preferred label, then suggested next ids, then unconditional edges, then highest weight with a lexical tiebreak on target id. Steps two through five draw only from unconditional edges. If no step yields an edge, the run halts with a no_matching_edge pipeline error. 1 Condition-matching edges condition="outcome=retry" · key=value · != · && 2 preferred_label match the node named the edge it wants to take 3 suggested_next_ids the node named a target node directly 4 Unconditional edges a plain a -> b with nothing attached 5 Highest weight, then LEXICAL tiebreak on target id alphabetical order breaks a tie between equally eligible edges no match steps 2–5 draw only from unconditional edges — an edge whose condition failed is never picked by label or id Nothing resolved — the run halts, loud PIPELINE_ERROR · error_type=no_matching_edge
Diagram 9 — first rule that resolves, wins. Steps 2 through 5 draw only from unconditional edges, so an edge whose condition failed can never be picked back up by label or by suggested id. And step 5 does not always resolve: when nothing is eligible the engine refuses to guess and terminates the run with no_matching_edge — a deliberate, ledgered divergence from the spec, which would have read the same dead end as a successful completion.

The documented pitfall, and what it does now

The story the older docs tell: an agent reported success with no preferred_label. Neither outgoing condition matched. Steps 1 through 4 all fell through, so step 5 ran — and the lexical tiebreak silently picked Fix over Test, because F sorts before T. The pipeline looped forever, fixing something that was already fixed.

That run cannot happen on the engine as it stands. With both outgoing edges conditional and neither condition true, there is nothing eligible left to choose from — steps 2 through 5 see unconditional edges only — and the run stops on the spot with no_matching_edge. The lexical tiebreak's remaining territory is narrower and louder: several conditional edges matching at once, or several unconditional edges tied on weight.

The advice is unchanged and the reason is better. Defensive inequality routing — state the negative case explicitly and weight it — makes step 1 resolve either way. Routing that is total never reaches the tiebreak, and never reaches the hard fail either.

// make the routing total — step 1 resolves either way
review -> Fix  [condition="outcome=retry"]
review -> Test [condition="outcome!=retry", weight=10]

The report_outcome contract

An LLM node communicates its result back to the orchestrator by calling a tool. This is the authoritative channel — everything else is recovery.

{
  "status": "success | fail | partial_success | retry",
  "preferred_label": "gate_pass",
  "suggested_next_ids": ["review"],
  "context_updates": { "artifact_path": "src/word_counter.py" },
  "notes": "two tests still failing on unicode input",
  "failure_reason": null
}

If the tool was not called, the orchestrator walks a recovery ladder, each rung less trustworthy than the last.

The verdict recovery ladder Five fallbacks below the authoritative report_outcome tool call, ending with plain prose which means success on ordinary nodes but retry on goal gate nodes, and an empty response which means fail. recovery order report_outcome tool call authoritative fenced JSON block in the response pure JSON response body embedded verdict recovery — a verdict found inside prose plain prose, no structure at all SUCCESS on an ordinary node RETRY on a goal_gate node empty response FAIL
Diagram 10 — the ladder is why fail-closed matters. The lower rungs are guesses about intent, and the second-to-last rung is where a goal gate refuses to guess in the direction of "done".

Context fidelity: what the next node gets to see

Each node declares how much of the run's history it inherits. This is the main cost/coherence dial in a long pipeline.

context fidelity modes
ModeWhat the node receivesNotes
fullFresh spawn, with the prior exchanges replayed in as parent_messagesTranscript replay, not session reuse — thread_id groups nodes onto one shared replayed transcript
compact (default)Fresh session plus a structured summaryThe sane default: independence without amnesia
truncateGoal and run ID onlyMaximum independence — useful for critics
summary:lowSummary at roughly 600 tokensExplicit budget when compact is too coarse
summary:mediumSummary at roughly 1,500 tokens
summary:highSummary at roughly 3,000 tokens

Gotcha

last_response is truncated to 200 characters in every mode — full included. What a full-fidelity successor gets instead is the replayed transcript, which carries the complete history; last_response stays a 200-character snippet regardless. If a downstream node is quietly making decisions on a fragment, this is usually why.

Retries and failure routing

The retry policy draws a hard line between "try again" and "this failed."

  • max_retries retries RETRY outcomes, transient errors (429s, 5xx, timeouts), and must_write violations — a node that promised an artifact and did not produce one.
  • A plain FAIL is returned immediately and is never retried. Failure is a routing decision, not a flake.
  • A FAIL routes only through an explicit path: an edge with condition="outcome=fail", a retry_target, or a node with runs_on=always / runs_on=failure. If none exists, the pipeline terminates — fail-fast by default.
  • continue_on_fail=true exists for the cases where you mean it, but it cannot override a must_write= artifact contract. You do not get to declare success over a missing artifact.

The rest of the instrument panel

model_stylesheet — CSS for model assignment

Assign models by selector instead of repeating llm_model= on forty nodes. Only three properties apply: llm_provider, llm_model, reasoning_effort.

* { llm_model: claude-sonnet-*; }
.reasoning  { llm_model: gpt-...; reasoning_effort: high; }
#final_check { llm_model: claude-opus-*; }

loop_restart — a real iteration, not just a jump

An edge with loop_restart="true" increments $iteration, resets completed nodes so they can run again, and preserves context_updates. The graph goes back to the start of the loop; what was learned does not.

feedback_from — teaching the next attempt

feedback_from= pipes a critic's output into a later node as $prior_critiques_<node_id>. Each critique is truncated to 500 characters, at most 5 are carried. The point is subtle and important: the next iteration runs in a fresh worker context, so this is the channel by which a brand-new worker learns what the last one got wrong.

Variables and parallelism

Prompts and commands expand $goal, any $param passed at launch, and $iteration. Fan-out uses a component node; the join uses join_policy=wait_all, with max_parallel defaulting to 4.

05A full run-through

Four nodes, two iterations, one real test run

This is the whole program. A dozen lines of DOT, no accompanying Python, no step definitions elsewhere. Read it, then watch it execute.

digraph convergence_loop {
    graph [goal="Write a Python function count_words(text) ..."]
    start     [shape=Mdiamond]
    implement [prompt="... If test_output.txt exists, read it ..."]
    test_gate [shape=parallelogram,
               tool_command="pytest -q test_word_counter.py > test_output.txt 2>&1 && echo gate_pass || echo gate_fail",
               goal_gate=true]
    done      [shape=Msquare]
    start -> implement -> test_gate
    test_gate -> done      [condition="context.tool.last_line=gate_pass"]
    test_gate -> implement [condition="context.tool.last_line=gate_fail", loop_restart="true"]
}

Three things in that listing carry all the weight. The goal lives in graph [goal=...], so the pipeline is self-describing. The gate is a shell command, so its verdict is an exit status rather than an opinion. And the worker's prompt tells it to read test_output.txt if it exists — which is how attempt two learns from attempt one.

Two iterations of the convergence loop First pass, iteration zero: implement writes code, the gate runs pytest and echoes gate_fail, and a loop restart edge wraps around, taking iteration to one. Second pass: implement reads the test output, the gate echoes gate_pass, and the run reaches done. first pass implement spawns a loop-agent sub-session writes count_words + tests test_gate pytest -q → test_output.txt gate_fail loop_restart=true — $iteration 0 → 1, completed nodes reset context_updates preserved — what was learned survives the reset second pass implement reads test_output.txt the evidence from attempt 1 test_gate pytest -q → test_output.txt gate_pass done exit node checks goal_gate — satisfied by real evidence — the run completes checkpoint written after every node
Diagram 11 — the same two nodes run twice. Nothing about the graph changed between iterations; what changed is that test_output.txt now exists.

Step by step

showing all 9 steps
  1. parse · transform · validate · initialize

    The engine reads pipeline.dot, runs its lint rules and reachability checks, resolves the stylesheet and expands variables, then builds the run context — including $goal, taken straight from the graph attributes. Nothing is checkpointed yet; the first checkpoint is written after the first node has actually run.

  2. first pass · implement

    A box node, so the orchestrator spawns a loop-agent sub-session with tools. Inside it: call the model, run the tools it requests, feed results back, repeat. It writes count_words and a test file, then reports an outcome. Checkpoint.

  3. first pass · test_gate

    A parallelogram, so no model is involved. The shell runs pytest -q, redirects stdout and stderr into test_output.txt, and echoes gate_pass or gate_fail depending on pytest's exit status. Two tests fail. The last line is gate_fail.

  4. edge selection

    Rule 1 resolves immediately: the edge with condition="context.tool.last_line=gate_fail" matches. No fallback rules run, so nothing is left to alphabetical order. The target is implement.

  5. loop restart

    That edge carries loop_restart="true". $iteration becomes 1 — the counter starts at 0 for the initial pass, so the second pass is iteration one — completed nodes are reset so they may run again, and context_updates are preserved: the run rewinds its position, not its knowledge.

  6. second pass · implement

    A fresh worker context, and this is the point of the whole design: it does not inherit the previous worker's reasoning about why the code was right. Its prompt tells it to read test_output.txt if present, so it reads the actual failure output — the assertion, the traceback, the line number — and fixes the bug.

  7. second pass · test_gate

    The identical shell command runs again. pytest exits 0, so the && branch fires and the last line is gate_pass.

  8. route to exit

    Rule 1 resolves again, this time onto the done edge. done is an Msquare, which is not merely a label — reaching it triggers the goal-gate check.

  9. goal gate · finalize

    The goal gate reported success through a real command's exit status, not through prose, so the fail-closed rule has nothing to object to. The run completes with a passing test suite on disk as its artifact.

Why the redirection matters — lint rule CMD-001

The command writes to a file with > and then tests the exit status. Piping instead — pytest | tail or pytest | grep — would hand the shell the pipe's exit status, which is the last command's, and pytest's failure would vanish. The gate would report gate_pass on a red test suite, and the loop would exit on a lie. Attractor lints for this.

The companion hazard — a failed tool node leaves tool.last_line stale

A tool node that fails does not refresh tool.last_line: the value from its last successful run is still sitting in the context, and a label-routed edge will match against it happily, on evidence from a previous pass. Where a gate's command can genuinely exit non-zero, pair the label test with the node's own outcome — condition="context.tool.last_line=gate_pass && outcome=success" — so stale evidence cannot route the run on its own.

The sample above does not need that guard, and the reason is the idiom: … && echo gate_pass || echo gate_fail always exits 0, so the node always succeeds and tool.last_line is always this pass's. Write a gate that can exit non-zero and you have opted back into the hazard.

06How you use it

Four entry points

01As an Amplifier bundle

Point your configuration at a graph with dot_file: and run. There is no --goal flag, because the goal is an attribute of the graph — the pipeline carries its own intent.

02As a standalone CLI

The attractor binary runs a graph directly and passes parameters in.

attractor run pipeline.dot --param goal="Fix the TypeError..."

attractor resume run-dir/       # pick up after the last completed node
attractor lint   pipeline.dot   # structural checks, CMD-001 and friends
attractor trace  run-dir/       # what path did a run actually take
attractor doctor                # environment and provider diagnostics
03Conversationally, with the run_pipeline tool

Inside a session you can ask for a pipeline by name and let the assistant launch it:

"Run the plan-implement-test pipeline to add input validation to the login endpoint."

04The /attractorify slash command

Analyses the session you are already in against the three-question test below, tells you whether the work actually wants a pipeline, and then designs one with you conversationally. It is as willing to answer "no" as "yes".

The bundle is also importable as a library, so a graph can be executed programmatically from your own code when neither the CLI nor a session is the right host.

07When to reach for it

The three-question test

Attractor is a specific tool for a specific shape of problem. If a task does not answer yes to all three, a pipeline is probably ceremony. The repository is careful about how hard to press that: a "no" is a signal to reconsider the shape of the work, not a verdict on it.

  1. Is there a cycle?

    If the work is a sequence with no path backwards, there is nothing to converge to. A graph without a cycle is a list with extra syntax.

  2. Is the exit gated on machine-checkable evidence, external to the worker?

    Tests, builds, type checks, schema validation. If the only thing standing between the run and "done" is a model's assessment of its own output, the loop guarantees nothing.

  3. Would it still land if any one LLM node had a bad day?

    The failure mode to design against is not a crash — it is a single response that is plausible but wrong. If one such response can end the run successfully, the graph is not doing its job.

Reach for a recipe

Staged, sequential workflows with human approval gates. Known steps, known order, a person in the loop deciding whether to continue.

Reach for an attractor

Machine-verified convergence. Unknown number of attempts, a gate that cannot be argued with, and a corrective path back into the work.

"If your pipeline graph has no cycle, it should probably have been a recipe."