Rivet documentation
Rivet runs one business process at a time: new leads, a shared inbox, repeat team questions. Each worker reads the item, decides what it is and who owns it, and acts in the tools you already use. A deterministic policy decides what runs on its own and what waits for a person. Every action goes in an audit log, and a scorecard shows how much the worker handled.
How it works
item (form, email, chat, webhook)
│
▼
┌───────────┐ tool call ┌──────────────┐ execute ┌───────────────────────────┐
│ Claude │ ────────────► │ policy gate │ ──────────► │ Slack, HubSpot, n8n/Make, │
│ (worker) │ ◄──────────── │ (your YAML) │ │ allowlisted APIs, docs │
└───────────┘ result └──────┬───────┘ └───────────────────────────┘
│ hold
▼
approval queue ──► person approves ──► exact action runs
│
runs.jsonl audit log ──► scorecard The worker file
A worker is one YAML file: what the process is, who can own items, which tools it may use, and how much it may do on its own.
name: lead-intake
description: New inquiries are checked for fit and urgency and handed to the right owner.
autonomy: 2 # acts, but risky actions wait for a person
instructions: |
Each item is one new inquiry. sales: new projects and pricing. support: existing clients.
Send it to review when there is no way to reply or you cannot tell what they want.
owners:
- id: sales
name: Sales (Maya R.)
slack_channel: "#sales-leads"
- id: support
name: Client support (Dev K.)
slack_channel: "#support"
tools: [route, request_review, slack_post_message, crm_upsert_contact]
policy:
approval_required: [crm_upsert_contact]
review:
slack_channel: "#rivet-review" Starter workers
- lead-intake
- Sorts new inquiries by fit, routes them to sales, partnerships or support with a summary, and saves the contact to HubSpot after approval.
- inbox-triage
- Sorts shared-inbox email by type and urgency, routes it to an owner, and hands the label back to your n8n or Make flow.
- team-answers
- Answers repeat internal questions from your own docs, names the source, and sends gaps to a person instead of guessing.
Guardrails
The model never decides what it is allowed to do. Rivet does, from the worker file.
- L0 Manual
- The item is logged. Rivet takes no action.
- L1 Suggests
- Every action that changes something outside Rivet waits for a person.
- L2 Acts with approval
- The worker acts. Tools in policy.approval_required wait for a person.
- L3 Acts on its own
- Routine work runs. Unclear items still go to review.
- L4 Handles exceptions
- Like 3, plus exceptions your instructions explicitly cover.
On top of the level:
- always_approve: holds a tool at every level.
- Budgets: max_tool_calls and max_turns stop runaway loops. A worker that runs out of turns sends the item to review.
- Named connections only: webhook_send picks a webhook by name, never a URL, and http_request only reaches HTTPS hosts in allowed_hosts, with redirects refused.
- Routing is constrained: the route tool only accepts the owner ids in the worker file, and a routing worker that finishes without choosing an owner sends the item to review.
- Refusals go to review: if the model declines an item, it lands in front of a person instead of disappearing.
- Approvals are exact: approving runs the stored action with the same tool and the same input. Nothing is regenerated.
- Inputs are data: forms and emails are wrapped as data in the prompt and treated as untrusted.
Quick start
Requires Node 20+ and a Claude API key.
$ git clone https://github.com/h-vance/rivet && cd rivet
$ npm install && npm run build
$ export ANTHROPIC_API_KEY=... # Claude API key
$ export SLACK_BOT_TOKEN=xoxb-... # or SLACK_WEBHOOK_URL
$ export HUBSPOT_TOKEN=pat-... # only for crm_upsert_contact
$ node dist/cli.js validate workers/*.yaml
$ node dist/cli.js run workers/lead-intake.yaml --input examples/lead.json
$ node dist/cli.js approvals # anything held for a person
$ node dist/cli.js approve apr_... # runs the exact held action
$ node dist/cli.js scorecard --days 30 Run it as a service
Point your form, inbox or n8n/Make/Zapier flow at it. The server binds to 127.0.0.1 by default; put it behind HTTPS before exposing it.
$ export RIVET_TOKEN=$(openssl rand -hex 24)
$ node dist/cli.js serve --workers workers --port 8787
$ curl -X POST http://127.0.0.1:8787/workers/lead-intake/run -H "Authorization: Bearer $RIVET_TOKEN" -H "Content-Type: application/json" -d @examples/lead.json - POST /workers/:name/run
- Run a worker on one item (JSON body). Returns the run record.
- GET /approvals?status=pending
- List held actions.
- POST /approvals/:id/approve | reject
- Decide one. Body: {"by": "maya", "note": "..."}
- GET /scorecard?worker=&days=30
- Handled automatically, sent to review, overridden, failed.
- GET /health
- Liveness (no auth).
Where data lives
Everything is plain files under RIVET_HOME (default ./.rivet):
- runs.jsonl: one line per run (input, every tool call and its outcome, the routing decision, token usage, and the model that served it).
- approvals/<id>.json: one file per held action and its decision.
Nothing is sent anywhere except Claude and the connectors you configure. AI calls use your own API key.
Models
Workers default to claude-opus-5-5 at effort: low, which is plenty for sorting and
routing. Change model or effort per worker. The system prompt is cached across runs.
Scorecard
Rivet scorecard: worker lead-intake, since 2026-10-10
Runs 214
Handled automatically 171 (79.9%)
Sent to review 31 (14.5%)
Waiting on approval 12 (5.6%)
Failed runs 0 (0%)
Approvals decided 58 (12 pending)
Overridden by a person 3 (5.2% of decided) Illustrative numbers. Use it to decide when a worker has earned the next autonomy level.