Skip to main content

rote

Graduate fuzzy AI skills into deterministic, reliable workflows.

CI PyPI Python versions License: Apache 2.0

rote is a CLI that takes an Anthropic-style Skill (a SKILL.md plus references/) and turns it into a runnable background pipeline in one shot. An LLM agent (itself defined as a skill) reads the source skill, applies a structured graduation rubric, and emits a runtime-agnostic intermediate representation (pipeline.yaml), extracted Python modules for the deterministic parts, typed signature stubs for the LLM-judge parts, and runnable code for the durable execution engine of your choice.

pip install rote-cli    # or zero-install: uvx --from rote-cli rote ...

# `rote graduate` runs an LLM agent, so it needs a driver: Claude Code
# (`claude`) or Codex (`codex`) installed and authed, or ANTHROPIC_API_KEY
# for the in-process `api` driver. The BDR run below takes ~13 min and
# ~$0.70 with Sonnet. (`rote emit` needs no LLM — see below.)

# Default target is DBOS — durable execution as a plain Python library,
# no orchestrator to run, SQLite for dev / Postgres for prod:
rote graduate ./examples/bdr-outreach/skill --out ./graduated/

# Or pick another runtime (see the table below):
rote graduate ./examples/bdr-outreach/skill --runtime temporal   --out ./graduated/
rote graduate ./examples/bdr-outreach/skill --runtime cloudflare --out ./graduated/

The name comes from rote learning — doing something so many times, so reliably, that it becomes mechanical. That's what graduation does to a skill: a fuzzy 10–20 minute agent loop becomes a deterministic pipeline that runs in the background, costs a fraction of the tokens, and can be regression-tested.


Why

Fuzzy AI skills work, but in production they're slow (a 10–20 minute agent loop is unacceptable as a background job), expensive (multi-agent loops use ~15× the tokens of a single chat, mostly re-deriving procedures the author already wrote down), and non-deterministic (a "MANDATORY" check enforced only by prose can be silently skipped, and there's no way to regression-test a behavior the LLM has to remember).

The fix is to separate the parts of a skill that are actually fuzzy from the deterministic procedures wearing fuzzy clothing. Move the deterministic parts into code, keep the LLM only where the input is genuinely unbounded (parsing, classifying, drafting), and wrap the whole thing in a durable execution engine with explicit human-in-the-loop gates. That graduation step is what rote automates.

There's third-party data for what this buys. "Compiled AI: Deterministic Code Generation for LLM-Based Workflow Automation" (Trooskens et al., Apr 2026) measured compiling LLM workflows into deterministic code: 57× fewer tokens at 1,000 transactions, 450× lower median latency, 100% reproducibility (vs. 95% for direct inference at temperature 0), and ~40× lower TCO at a million transactions a month. The multiples grow with volume — once a workflow is proven, every run through an agent loop pays LLM prices for work code does for free.

A distinction worth being precise about: durable-execution vendors make fuzzy agents durable (wrap the loop in retries and state so it survives crashes — still fuzzy inside). rote removes the fuzzy loop. The two compose: Temporal, Cloudflare Workflows, and the rest are rote's compile targets, not its rivals.

When not to use rote: exploratory and one-off work should stay an agent loop — flexibility is the whole point there, and there's nothing proven to compile yet. rote is for the skill you've run twenty times and want to run a thousand more, unattended.


How it works

rote is a three-layer system; each layer has one job and contracts on a small interface.

   SKILL.md + references/          Source skill bundle (untouched)
             │  rote graduate
             ▼
   graduator agent                 An LLM agent (Claude / Codex /
   (pluggable driver)              Anthropic SDK) runs the rote-graduate
             │                     skill against the source bundle.
             │  filesystem contract: work_dir/pipeline.yaml
             ▼                     + extracted/ + signatures/
   Pipeline IR (pipeline.yaml)     Pydantic-validated DAG of typed
             │                     nodes. Five node kinds. Runtime-agnostic.
             │  rote.adapters.<runtime>
             ▼
   emitted runtime code            Native code for the target durable
                                   execution engine.
  1. The graduator agent (skills/rote-graduate/) — a regular Anthropic Skill (SKILL.md + four reference files). This is the brain; it runs inside any Skills-compatible surface, and you don't need rote to use it.
  2. The IR (src/rote/ir.py) — Pydantic models for the five node kinds plus edges, retries, HITL gates, and metadata. The IR is the source of truth; everything downstream is template substitution.
  3. Runtime adapters (src/rote/adapters/) — pluggable modules that consume an IR and emit runnable code for one engine.

The graduator's job ends when it has produced a valid pipeline.yaml. Code emission is deterministic Python — never agent-driven — so the same IR always produces byte-identical output.


Quickstart

From Claude Code (recommended)

rote ships as a Claude Code plugin, so you can graduate a skill without touching Python tooling:

/plugin marketplace add trevhud/rote
/plugin install rote@rote

Then say "graduate this skill" (or run /rote:graduate). It confirms the source directory, asks which runtime you want, runs the CLI via uv in the background, and reports the emitted pipeline. A second skill, /rote:serve, wires graduated pipelines up as MCP tools so Claude can trigger the deployed workflows (see docs/mcp-trigger.md).

Prefer a terminal? The same thing is one uvx command:

uvx --from rote-cli rote graduate ./my-skill --runtime dbos --out ./graduated

Naming note: the rote package on PyPI is an unrelated memoization library that also installs import rote, so the two can't share an environment. This project's distribution is rote-cli while the CLI command and import name stay rote — hence uvx --from rote-cli rote .... See docs/releasing.md.

Run on the bundled example

The repo includes a real BDR outreach skill (lead generation, contact vetting, CRM upload, mandatory exclusion checks, email personalization, manual enrollment handoff) in examples/bdr-outreach/skill/:

rote graduate examples/bdr-outreach/skill --out /tmp/bdr-graduated

On that skill the graduator produces a 22-node IR that's 78.9% codifiable (15 of 19 non-gate nodes), extracts 5 Python modules and 2 typed judge signatures, and flags 4 mandatory nodes and 3 HITL gates — in ~13 minutes for ~$0.70 (Sonnet via Claude Code). Along the way it independently lifts the three MANDATORY exclusion checks out of prose, pulls four batch-size constants out of prompt text, and models a parallel entry path the hand-written baseline missed.

rote auto-detects a driver in the order claudecodexapi; override with --agent. The output directory splits into graduated/ (the agent's pipeline.yaml, extracted/, signatures/, eval seeds, and a graduation-report.md) and runtime/<runtime>/ (the adapter's emitted code + a README on how to run, signal gates, and deploy).

Other commands

  • rote emit <pipeline.yaml> --out <dir> — run just the adapter step on an existing IR (no LLM, no cost). The cheap inner loop while iterating on adapters or IR shapes. Re-emitting is safe: a .rote-manifest.json tracks what rote wrote, and files you've edited are left untouched (the fresh version lands as <name>.new).
  • rote graduate --update — re-graduate incrementally when the skill changes. rote diffs the skill against the previous run's provenance.json and re-derives only the nodes whose source sections changed; unchanged nodes keep their ids (so in-flight durable workflows aren't orphaned) and implemented stubs are kept. No change → no agent run.
  • rote run <path> — one-off local execution of either side. A skill directory runs as an agent via claude -p (your registered MCP servers injected, read-only tool gate unless --allow-writes); an emitted runtime directory — or a graduate --out directory — runs the pipeline itself — all six runtimes (python/dbos/temporal in-process or on a managed local dev server, cloudflare under wrangler dev, inngest against a managed inngest-cli dev, dbos-ts against your Postgres or a throwaway Docker one). HITL gate payloads via --signal name='{...}' or an interactive prompt. Runtimes that bundle a dev UI surface it: temporal runs print a live Temporal Web UI URL and inngest runs print the dev-server dashboard, both live for the duration of the run. Output JSON on stdout, status on stderr, so it pipes.
  • rote deploy <path> — push an emitted pipeline where it runs: cloudflare wraps npx wrangler deploy (with --dry-run), dbos / dbos-ts wrap npx dbos-cloud app deploy — the vendor CLI owns auth and output; rote adds detection and preflights (including surfacing which account your wrangler session belongs to before uploading). Runtimes with no push model (temporal, inngest, python) print honest hosting guidance with doc links instead of a fake action. --target rote-cloud bundles a cloudflare-emitted app (esbuild via npx) and uploads it to a hosted rote-cloud instance — with a stored rote login, no flags or env vars needed (--url/--token and $ROTE_CLOUD_URL/$ROTE_CLOUD_TOKEN still override).
  • rote login — connect the CLI to a rote-cloud account via the OAuth device flow: your browser opens with a one-time code pre-filled (over SSH, --device prints the code + URL instead), you click Approve, and the CLI stores a tenant API key at ~/.local/share/rote/cloud.json (mode 0600). Once logged in, rote graduate runs on rote cloud by default: the skill bundle syncs up (sha-diffed, so unchanged files don't re-upload), the platform runs the graduation server-side, live progress streams back through the same renderer as a local run, the result auto-deploys, and the artifacts download into your --out directory in the exact local layout. --local keeps the graduation on your machine (then the cloudflare-emit + auto-deploy flow applies), --no-deploy or a config opt-out (runtime: pinned to a local target, or deploy: none) keeps everything local; --cloud forces the server even where config says otherwise. Logged out, everything works locally exactly as before. rote whoami shows the account (verified live); rote logout revokes the key server-side and clears the store.
  • rote init — one-time interactive onboarding: pick where graduated pipelines run (rote cloud — with login offered inline — or a local runtime, with a one-line pitch for each), which graduator driver does the work (availability probed live), and optionally a model. Answers are saved to ~/.config/rote/config.yaml (--project writes a ./rote.yaml that overrides it per-repo) and every later command reads them. It's the only interactive command besides login — CI never hits a prompt.
  • rote config — print every configurable default with its effective value and the layer that set it. Resolution everywhere is flag > ROTE_* env (ROTE_RUNTIME, ROTE_DEPLOY, ROTE_AGENT, ROTE_MODEL) > project rote.yaml > user config > built-in. Config files are strict: a typo'd key or value is a loud error, never a silent fallback. --json for automation.
  • rote eval <graduated> — render the before/after scorecard (wall clock, cost across the current model lineup at live prices, and how much of the run is still LLM-decided). rote graduate writes this to graduated/scorecard.md automatically. Add --run to measure instead of estimate: it executes both sides for real and appends measured cost, turns, and output agreement across trials.
  • Per-node inference — emitted judges read ROTE_MODEL_<ID> and ROTE_BASE_URL_<ID> at runtime, so you can swap the model or point at any OpenAI-compatible endpoint (Ollama, vLLM, a gateway) without re-emitting.

The five node kinds

Every step in a graduated pipeline is exactly one of five kinds. Full guidance: references/node-kinds.md.

Kind What it is Where the LLM lives
pure_function Fixed logic, deterministic I/O Not involved
external_call Vendor API call with fixed semantics + retries Not involved
llm_judge Fuzzy classification against a rubric, typed I/O Typed signature (DSPy/BAML in Python; Zod + vendor SDK in TS), from the IR's runtime-agnostic signature_spec
agent_loop Genuinely exploratory tool use Bounded agent loop
hitl_gate Explicit human approval, suspend until signal Durable suspend/resume

The guiding rule: keep the LLM at points where the input is unbounded or ambiguous, and codify everything else. When a step could go either way, prefer the more deterministic kind.


Runtimes

Pick with --runtime; the same IR drives all of them. Under --backend api, none of the emitted code references MCP — the crystallization step replaces tool calls with direct vendor API calls. Under the default --backend mcp, tool-using nodes emit a working MCP client call (with durable park-on-auth on every MCP-capable runtime); see docs/mcp-client.md.

Runtime --runtime Language Shape Notes
DBOS (default) dbos Python main.py@DBOS.workflow + @DBOS.step per node No orchestrator to deploy; SQLite (dev) / Postgres (prod)
Temporal temporal Python workflow.py + activities.py Signal handlers for HITL gates
Plain Python python Python single main.py script Max legibility, stdlib only; refuses HITL-gate pipelines
Cloudflare Workflows cloudflare TypeScript WorkflowEntrypoint + wrangler.jsonc wrangler deploy-ready
DBOS (TypeScript) dbos-ts TypeScript src/main.ts (DBOS Transact) Zero-orchestrator; Postgres-only
Inngest inngest TypeScript one inngest.createFunction Mounts into an existing Node/Next.js app; retries are function-level

Drivers

rote ships three interchangeable graduator drivers — pick whichever matches your auth. The same pipeline.yaml comes out either way.

Driver Backend Auth Install
claude (default) claude -p subprocess Claude Max/Pro OAuth or CLAUDE_CODE_OAUTH_TOKEN Install Claude Code separately
codex codex exec subprocess ChatGPT Plus/Pro OAuth Install Codex CLI separately
api anthropic Python SDK ANTHROPIC_API_KEY pip install 'rote-cli[api]'

The claude driver scrubs ANTHROPIC_API_KEY from the subprocess so a subscription login wins, and limits the agent to read/write/glob/grep tools. The default model is Sonnet rather than Opus — the task is structured-rubric-following, not deep reasoning, and Sonnet brings per-run cost from ~$3.50 to ~$0.70. Override with --model for skills where Opus earns its cost. Full design record, including the auth gotcha: docs/agent-runtime.md.

rote explicitly does not depend on claude-agent-sdk: Anthropic's ToS forbids third-party agents built on the Agent SDK from using claude.ai login credentials without approval, which would defeat the subscription path.


How it differs from other tools

  • vs. raw durable engines (Temporal / Cloudflare / Inngest / Restate): they give you the workflow runtime; they don't help you decide what should be a workflow. rote is the missing step that turns a working skill into something worth running on one.
  • vs. LangGraph: LangGraph is an excellent state machine, but its graph is hand-built. rote produces a graph from prose, classifies nodes by determinism, and pushes work out of the agent loop wherever the data supports it.
  • vs. using Skills directly: Skills run great interactively. rote is what you reach for when a skill becomes business-critical and needs to run unattended with hard reliability guarantees and per-step regression tests.

Status

rote is pre-1.0. The end-to-end flow works on the BDR example. The fast suite (pytest tests/) makes no real API calls and is what CI runs on every push, alongside a Python e2e (DBOS over SQLite + the MCP server over real stdio). Each adapter also has a slow-marked e2e that runs its emitted code against the real runtime (Temporal's time-skipping server, the TypeScript targets via tsc --noEmit and live dev servers, the plain-Python subprocess); those need a Node toolchain / Docker, so they run locally with pytest tests/ -m slow, not in CI.

Known gaps: the extracted modules are NotImplementedError stubs you fill in with real API-client code, a Restate adapter is planned, and fan_out nodes currently receive the whole upstream list in one invocation (per-element dispatch is a planned enhancement). Published on PyPI as rote-cli via tag-driven Trusted Publishing (docs/releasing.md).


Repository layout

rote/
├── docs/                  agent-runtime · mcp-client · mcp-trigger · releasing
├── skills/rote-graduate/  the graduator agent (SKILL.md + 4 reference files)
├── src/rote/
│   ├── cli.py             rote graduate / emit / eval / serve
│   ├── ir.py              Pydantic IR models + load_pipeline
│   ├── graduator/         orchestrator + drivers/ (claude · codex · anthropic_api)
│   └── adapters/          dbos · temporal · python · cloudflare · dbos_ts · inngest
│                          (+ _common / _py_common / _ts_common emit helpers)
├── examples/
│   ├── bdr-outreach/      canonical: all 5 node kinds · IR baseline · run snapshots
│   ├── ops-report/        100% roteness: zero LLM nodes + a HITL gate
│   └── deal-monitor/      data-heavy: parallel waves · fan-out judges · template render
└── tests/                 fast + slow suites (pytest -m slow)

Documentation

  • AGENTS.md — operating manual for a coding agent driving rote as an installed tool (invocation contract, the slow/costs-money graduate flow, auth, failure recovery, the stub-filling job, --json)
  • docs/agent-runtime.md — design record for the driver abstraction (the claude -p env gotcha; the non-use of claude-agent-sdk)
  • docs/mcp-client.md — the OAuth MCP client emitted code uses under --backend mcp: endpoint/credential resolution and durable park-on-auth across every MCP-capable runtime
  • docs/mcp-trigger.mdrote register + rote serve: graduated pipelines as MCP tools (FastMCP 3.x)
  • docs/releasing.md — tag-driven PyPI Trusted Publishing
  • skills/rote-graduate/ — the graduator's SKILL.md and its four rubric files (node kinds, crystallization heuristics, IR schema, LLM-judge extraction)
  • examples/bdr-outreach/ — the canonical skill, its ground-truth IR, and snapshotted real graduator runs
  • examples/ops-report/ — the 100%-roteness archetype: every step deterministic, one durable HITL gate, zero LLM nodes after graduation
  • examples/deal-monitor/ — the data-heavy archetype: parallel entry waves, fan-out judges, and a template render replacing per-run LLM-generated HTML

Roadmap

In rough priority order:

  1. Re-graduate BDR end-to-end with signature_spec — the bundled IR was hand-extended with structured schemas; the rubric now teaches the field, but no real run has produced one yet.
  2. Pre-filter as a pure_function node — today hard thresholds are lifted into a judge's forward(), which works for Temporal but not Cloudflare; a separate node makes the short-circuit uniform.
  3. More example skills — BDR is one shape; research-heavy, retrieval-heavy, and code-review skills stress the IR differently.
  4. fan_out per-element dispatch — currently the whole upstream list arrives in one invocation.
  5. The graduator graduating itselfrote-graduate is a SKILL.md; pointing rote graduate at it should crystallize its rubric-grade pieces and leave only the genuinely fuzzy judgments in the loop.

Contributing

The most useful contribution right now is to run rote graduate on a real skill of your own and report what happens — the rubric was designed against one skill and needs more. Adding a runtime adapter or a graduator driver, or improving the rubric, are all good next steps. See CONTRIBUTING.md for dev setup, the test layout, and the adapter/driver how-tos.

License

Apache-2.0. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rote_cli-0.10.0.tar.gz (313.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rote_cli-0.10.0-py3-none-any.whl (368.2 kB view details)

Uploaded Python 3

File details

Details for the file rote_cli-0.10.0.tar.gz.

File metadata

  • Download URL: rote_cli-0.10.0.tar.gz
  • Upload date:
  • Size: 313.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for rote_cli-0.10.0.tar.gz
Algorithm Hash digest
SHA256 2b534668b4a322b72e0f090af8b0d0d9e20e91a533b19e003475247e41adcf48
MD5 ebeee051379b09395610d2ab37c6d553
BLAKE2b-256 a33f1d8f2bd22d569fcfef5239ae544c750013c4540db283352c645d580b161a

See more details on using hashes here.

Provenance

The following attestation bundles were made for rote_cli-0.10.0.tar.gz:

Publisher: release.yml on trevhud/rote

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rote_cli-0.10.0-py3-none-any.whl.

File metadata

  • Download URL: rote_cli-0.10.0-py3-none-any.whl
  • Upload date:
  • Size: 368.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for rote_cli-0.10.0-py3-none-any.whl
Algorithm Hash digest
SHA256 30ccdcf31bed7510951b0a010dbe5a11eb2dfab4f865f704a3685e510379c7bd
MD5 78bfdf5befaa0fb66c7edf96bd753709
BLAKE2b-256 f9a0638f89135b3b62178c1975f5ef7707bcc91dfde774ada22f0d7f9cdf2ce8

See more details on using hashes here.

Provenance

The following attestation bundles were made for rote_cli-0.10.0-py3-none-any.whl:

Publisher: release.yml on trevhud/rote

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page