Declare the pipeline once. Every request walks the same ordered list. Workers can pass, mutate, enrich, block, or branch. Declarative where it suffices, inline code where you need it.
A pipeline is an ordered list of workers grouped by stage. The stages are pre-request, post-response, and on-error. Within a stage, workers run in declaration order.
# config.yaml
workers:
- id: pii-scrub
stage: pre-request
- id: credential-inject
stage: pre-request
- id: metering
stage: post-response
- id: audit-log
stage: post-response
- id: rewrite-on-failure
stage: on-error
max_branches limit).workers:
- id: metering
stage: post-response
# Metering reads res.body.usage, computes cost, writes to the ledger.
# No other logic. The simplest useful pipeline.
workers:
- id: pii-scrub
stage: pre-request
config:
redact:
- email
- phone
- "\\b\\d{3}-\\d{2}-\\d{4}\\b" # SSN
- id: metering
stage: post-response
- id: audit-log
stage: post-response
# Redacts PII before the request leaves the gateway.
# Metering and audit record the full receipt downstream.
workers:
- id: rewrite-on-failure
stage: on-error
config:
on_status: [500, 502, 503, 504]
fallback_upstream: anthropic-direct
routes:
- match: 'request.model == "gpt-4o"'
upstream: openai
circuit_breaker:
failure_threshold: 5
reset_after: 30s
workers:
- id: inline-r1-classify
stage: pre-request
runtime: node
code: |
import { r1 } from '@relayone/r1-client';
export async function onRequest(req, ctx) {
if (req.body.messages.length < 3) return req;
const intent = await r1.call('classify-intent', {
prompt: req.body.messages.at(-1).content
});
ctx.annotate({ intent });
if (intent === 'code') req.upstream = 'anthropic-direct';
if (intent === 'data') req.upstream = 'openai';
return req;
}
- id: metering
stage: post-response
Workers declare their runtime with runtime: wasm | node | python. Default is wasm. Node workers use an isolated sandbox; Python workers use an embedded interpreter. Choose based on ecosystem needs, not performance: WASM is fastest, Node has the largest ecosystem, Python is best for data logic.
If a worker throws, RelayGate records the error in the receipt and continues with the remaining workers unless the worker declared fail_closed: true. A failing pre-request worker with fail_closed blocks the request; a failing post-response worker still lets the upstream response reach the client.
The relaygate test command replays a fixture request against your pipeline and asserts on the receipt. Use it in CI to catch policy drift before it reaches production.
relaygate test --fixture tests/req-01.json --expect '.redactions >= 1 and .overhead_us < 500'
Next: CEL policy reference →