Relay Docs
Access & governance

Runbooks

Run pre-approved, Git-reviewed statements with typed parameters, and make production connections reachable only through them.

A runbook is a pre-approved SQL statement or MongoDB operation kept in a Git repository. Relay syncs the repository, binds each runbook to the connections it may run on, and lets members run it with typed parameters. Combined with a runbook-only connection, production can be reached only through statements that went through code review, while staging stays free-form.

Runbooks are rolling out gradually and are included in the Business and Enterprise plans. In the Agent Edition the license must include the runbooks feature. Runbooks run on PostgreSQL, MySQL, SQL Server and MongoDB connections; see Engine support.

Writing a runbook

Every .sql file under the configured directory is a runbook. It starts with a YAML header written as -- comment lines between two -- --- fences, followed by exactly one statement:

-- ---
-- name: find_customer
-- description: Look up one customer by id
-- engine: postgres
-- connections: ["prod-*", "staging"]
-- mode: read
-- timeout: 10
-- max_rows: 100
-- params:
--   - name: customer_id
--     type: int
--     description: Customer id
--   - name: status
--     enum: [active, closed]
--     default: active
-- ---
SELECT id, email FROM customers WHERE id = $1 AND status = $2
KeyMeaning
nameLowercase letters, digits, ., _, -. Defaults to the file name.
descriptionShown in the catalog.
engineOptional. A runbook for one engine is refused on connections of another.
connectionsRequired. Globs on connection names (prod-*).
moderead (default) cannot change data (see Engine support); write allows changes.
timeoutStatement timeout in seconds (default 30, max 300).
max_rowsResult cap (default 500, max 5000). Results are also capped at 1 MiB.
paramsTyped parameters: string, int, float, bool, date (YYYY-MM-DD), timestamp (RFC 3339), uuid. A parameter with a default is optional; enum restricts the values.

Parameters bind to the statement's placeholders in the order they are declared. A file runs exactly one statement: SELECT 1; DROP TABLE x fails. Every declared parameter must have a placeholder, and the other way around.

Engine support

MongoDB runbooks use a different, declarative file format; see MongoDB runbooks. The table below covers the SQL engines.

PostgreSQLMySQLSQL Server
Placeholders$1, $2, …?@p1, @p2, …
How values reach the databaseBound values (extended protocol)Typed literals, see belowTyped sp_executesql parameters
mode: readRead-only transaction; writes failRead-only session; writes and DDL failRuns in a transaction that is always rolled back
timeoutstatement_timeoutmax_execution_time (max_statement_time on MariaDB)Lock timeout plus an agent deadline
Affected rowsCommand tagROW_COUNT()@@ROWCOUNT

On PostgreSQL and SQL Server, values are sent as bound values and are never spliced into the statement text, so a parameter cannot change what the statement does.

On MySQL, prepared statements are refused when masking is on, so the agent sends the statement as text and keeps masking, policy and audit on exactly what runs. It encodes each parameter from its declared type, not from the raw text: numbers and booleans as digits, and strings, dates, timestamps and UUIDs as hexadecimal literals. A value therefore cannot contain a quote, backslash, comment marker or placeholder, and the connection's sql_mode (NO_BACKSLASH_ESCAPES, ANSI_QUOTES) cannot change how it is read. Invalid UTF-8, non-finite numbers and values that do not parse as their type are rejected before anything is sent.

SQL Server has no read-only session mode. A read runbook runs inside a transaction that is rolled back at the end, so its writes are undone rather than refused, and effects that a transaction cannot undo are not covered. Give the connection's database login only the permissions its runbooks need.

MongoDB runbooks

MongoDB runbooks are declarative: a *.mongo.yaml, *.mongo.yml or *.mongo.json file describes one operation on one collection. They do not contain JavaScript. Relay does not run scripts: the agent is a wire-protocol proxy, and a script could build commands that nobody reviewed. A declarative file maps to exactly one database command, which is what masking, policies and audit see and what the Git reviewer reads. A .js file in the runbook directory is rejected at sync with an error naming the file.

name: recent_orders
description: Orders of one customer above a minimum total
connections: ["orders-*"]
mode: read
timeout: 10
max_rows: 100
params:
  - name: customer
    type: objectid
  - name: min_total
    type: float
    default: "0"
database: shop
collection: orders
operation: find
filter:
  customer_id: {$param: customer}
  total: {$gte: {$param: min_total}}
sort: {created_at: -1}
projection: {items: 0}

The header keys are the same as for SQL runbooks. engine is optional and must be mongodb. In addition:

KeyMeaning
databaseOptional. Defaults to the connection's database.
collectionRequired. system.* collections are refused.
operationfind, aggregate, count, distinct (read) or insertOne, updateOne, updateMany, deleteOne, deleteMany (need mode: write).
filterQuery document. Required and non-empty for update and delete operations.
sort, projectionfind only.
pipelineRequired for aggregate: a list of stages.
fieldRequired for distinct.
documentRequired for insertOne.
updateRequired for updateOne and updateMany. Only update operators ($set, $inc, ...); a replacement document is refused.

Place a parameter anywhere a value goes with {$param: name}. Parameters are typed and sent as BSON values: int becomes a 64-bit integer, float a double, bool a boolean, date and timestamp a BSON date, uuid a binary UUID, objectid an ObjectId (a type available to MongoDB runbooks only) and string a string. A parameter is never parsed as JSON and never becomes a key or an operator, so a value such as {"$ne": null} is just a string. An optional parameter without a value is sent as null. In an aggregate runbook a string parameter that starts with $ is refused, because a pipeline would read it as a field path.

These are refused when the file is synced: $where, $function and $accumulator (they run JavaScript on the server), $out and $merge, and operations that change data without mode: write. In read mode only find, aggregate, count and distinct are accepted, and the agent enforces this again before running.

The agent runs the operation through its own MongoDB proxy as the person who requested it, so masking, policies and query audit apply as they do to a tunnel. It sets maxTimeMS from timeout, and fetches at most max_rows documents in a single batch (and 1 MiB); a larger result is cut and marked truncated. Each document is one result row, as relaxed Extended JSON in a document column. count returns a count column and distinct a value column. A write reports the number of documents matched, inserted or deleted, or modified for updates, in the command tag.

Connecting the repository

In Runbooks → Source, set the clone URL, the branch (default main) and an optional directory. Choose a poll interval (1 minute to 24 hours) or sync manually with Sync now or relay runbook sync. A source authenticates in one of two ways.

HTTPS with a token. Set an access token with read access (a GitHub or GitLab token or deploy token). The token is stored encrypted and is never shown again.

SSH with a deploy key. Use an ssh://git@host/org/repo.git or git@host:org/repo.git URL. Relay generates an Ed25519 key pair when you save: the private key is stored encrypted and never leaves the server, and the source shows the public key for you to add to the repository as a read-only deploy key. Tick Generate a new deploy key to rotate it.

An SSH source must pin the server's host key, and Relay refuses any server that presents another one. Select Fetch host key: Relay connects once and shows the key type and its fingerprint. Compare the fingerprint with the one your Git provider publishes, then pin it. You can also paste a public key line (ssh-ed25519 AAAA...) or a SHA256: fingerprint. The CLI shows the same with relay runbook source host-key <ssh-url>.

Relay resolves the repository host itself and refuses loopback, link-local and cloud metadata addresses, and private ranges outside the Agent Edition, for both HTTPS and SSH sources.

A file with an error is skipped and listed in the sync result; it keeps its last good version. Deleting a file retires the runbook.

Sync on push

Instead of waiting for the poll interval, let your Git host tell Relay about each push. On the source, open Push webhook and select Generate secret. Copy the secret now, it is stored encrypted and shown only once, and add a webhook to the repository that posts to the payload URL:

ProviderSetup
GitHubContent type application/json, the secret, the push event.
GitLabThe secret in Secret token, the Push events trigger.
Anything elsePOST JSON with "ref": "refs/heads/main" and an X-Relay-Signature-256: sha256=<hex> header, the hex HMAC-SHA256 of the raw body using the secret.

Relay verifies the signature and answers 401 otherwise. Only pushes to the source's configured branch start a sync; other branches, tags and branch deletions are ignored. Syncs are debounced to one every few seconds, and a push inside the window is still picked up by a trailing sync, so the latest push is always covered. The webhook carries no content: it only makes Relay fetch the branch, exactly as Sync now does. Rotate secret replaces the secret and Turn off disables the endpoint.

Versions

Each version is identified by the hash of its file, and records the commit it first appeared in. When you run a runbook, the run is pinned to the version you reviewed: if the file changed since you listed it, the run is refused until you review the new version.

Running a runbook

From the dashboard, open a runbook, fill in the generated form, pick a connection and run it. From the CLI:

relay runbook list -c pg-prod
relay runbook show find_customer
relay runbook run find_customer -c pg-prod --param customer_id=42
relay runbook history find_customer

You need access to the connection, exactly as for a tunnel (an access grant unless you are an admin), and the org.runbooks.run permission. The statement runs on an agent that serves the connection, with the connection's own database login. Masking, policies and query audit apply exactly as they do to a tunnel.

Result rows are visible to the person who ran the runbook and to runbook managers. The execution history shows everyone's runs without their rows.

Running from automation

An API key can run runbooks, for example from CI. The key needs the org.runbooks.run permission through its role and its workspace, like any member. The run then executes as the user who created the key: that user's connection access, masking and policies decide what the run can reach and see, so a key can never reach a connection its creator cannot. Keys created by another key, and keys whose creator was removed, cannot run runbooks. Audit-export keys never can.

To give automation a narrow identity, create a dedicated user, grant it only the connections its runbooks need, and create the key from that user's session.

Runs are attributed to the key in the activity log (api-key:<name>, with the creator as on_behalf_of) and on the execution (api_key). In the query audit, the requesting user is the creator and the execution id leads back to the key.

export RELAY_TOKEN=...   # the API key
relay runbook run find_customer -c pg-prod -p customer_id=42 --json

Runbook-only connections

Turn on Runbook-only on a connection to refuse tunnels to it for everyone except organization admins and owners. Open tunnels of other users end at the agent's next session report. Runbooks keep working on the connection.

Permissions

PermissionDefault rolesAllows
org.runbooks.readAdmin, Member, ViewerSee runbooks, the source and the execution history
org.runbooks.runAdmin, MemberRun runbooks, also with an API key
org.runbooks.manageAdminConfigure and sync the source, set runbook-only

Audit

Every run is recorded twice:

  • In the activity log, as run on the runbook, with the connection, commit, version and parameters.
  • In the query audit, as a runbook_execution event from the agent: the statement with its placeholders, the requesting user, the outcome (succeeded or failed) in decision, runbook:<name>@<commit> in rule_id, and a JSON reason with the execution id, commit, version, mode, parameters, row counts, truncation, error and duration. The statement's own query events share its connection id.

On this page