Docs

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.

workers/lead-intake.yaml
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.

terminal
$ 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.

terminal
$ 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.