Skip to content

Reference

Runbook YAML schema.

A runbook YAML is the declarative form of a long briar extract command line. Run it once with briar runbook extract, schedule it with briar runbook serve, or batch over a directory with briar runbook sweep. One file can describe many companies, each with many schedules, each running many extractors.

On this page

Minimal example

The smallest useful runbook — one company, one extractor, no schedule.

version: 1
companies:
acme:
knowledge:
store: file
name: knowledge:acme
extract:
- name: pr-archaeology
args:
pr_repo: [acme/widgets]
- name: active-tickets
args:
tracker: jira
ticket_project: [KAN]

Run it with briar runbook extract acme.yaml. Both extractors fire once; the result is written to ./knowledge/knowledge/acme.md.

Full-shape example

Every block populated. Use as a template and trim down to what you actually need.

version: 1
companies:
acme:
profile: production
knowledge:
store: postgres
name: knowledge:acme
config:
dsn_env: BRIAR_ACME_DATABASE_URL
git_identity:
name: Briar Agent
messages:
ticket_comment: { kind: jira-comment }
ops_chat: { kind: slack-channel }
escalation:
kind: telegram-chat
config:
chat_env: TELEGRAM_ACME_ALT_CHAT
mcp:
github:
transport: stdio
command: docker
args: ["run", "-i", "--rm", "-e", "GITHUB_TOKEN", "ghcr.io/github/github-mcp-server"]
env: { GITHUB_TOKEN: GITHUB_TOKEN }
tools: [search_issues, get_pull_request]
archetypes: [engineer]
sentry:
transport: http
url: https://mcp.sentry.dev/mcp
headers: { Authorization: SENTRY_MCP_BEARER }
extract:
- name: pr-archaeology
args:
provider: github
pr_repo: [acme/widgets, acme/api]
pr_max: 100
- name: aws-infra
args:
cloud: aws
aws_extract_region: us-east-1
aws_extract_service: [ecs, lambda, rds]
schedules:
- task: meetings
every: "day at 07:30"
extract:
- name: meeting-digest
- task: hourly-tickets
every: "1 hour"
extract:
- name: active-tickets
args:
tracker: jira
ticket_project: [ACME]

Top-level keys

version INTdefault: 1
Schema version. Only 1 ships today.
companies MAP
Map of company key → CompanyEntry. The company key is the token used in env-var interpolation — --company acme matches companies.acme and JIRA_ACME_TOKEN.

CompanyEntry

profile STRING
Free-form. Useful as a "production"/"staging" marker.
workspace_id STRING
Optional workspace identifier (passed through to providers that need it).
knowledge_file PATH
Legacy field. New configs use the structured knowledge: block below.
knowledge KnowledgeBinding
Where the company knowledge blob lands. See KnowledgeBinding below.
extract LIST[ExtractEntry]
Per-company default extractor list. It runs as an implicit schedule named extractors with cadence "day at 03:17". Ignored when a schedule already uses the task name extractors.
schedules LIST[ScheduleEntry]
Named groups of extractors with a cadence. Consumed by briar runbook serve (on the cadence) and briar runbook extract / sweep (once, now).
messages MAP[name, MessageBinding]
Named outbound-message channels. briar agent's send_message tool resolves names to writers at runtime.
git_identity GitIdentity
Per-company commit author for briar agent worktree commits.
mcp MAP[handle, McpServerBinding]
Named MCP servers whose tools the agent can call. See McpServerBinding below.

ExtractEntry

name EXTRACTORrequired
Extractor name. Must be a key in briar.extract.EXTRACTORS — see the registries page.
args MAP
Free-form arg map. Keys match the CLI flag names with - replaced by _ (e.g. pr-repo → pr_repo). The shared keys (repo, since_days, max, top_n, sample, authors_allow, ...) work too. See the mapping table below.

ScheduleEntry

task STRINGrequired
Schedule name. Used by briar runbook extract --task to fire one schedule on demand.
every STRINGrequired
Cadence expression parsed by EveryParser. See the full grammar in the cadence DSL section.
extract LIST[ExtractEntry]
Extractors that fire on this cadence.

KnowledgeBinding

store {file,postgres}default: file
filepostgres
Knowledge-store backend.
name BLOB_NAME
Blob name. When empty, briar uses knowledge_file if set, else ./knowledge/<company>.md, and in both cases writes with the file store even if store says otherwise. Schedules other than extractors write to <name>.<task> (e.g. knowledge:acme.nightly).
root PATH
File-store root override.
config MAP
Backend-specific config. The postgres backend reads dsn_env (the env-var name holding the DSN — never the DSN itself). Set inventory: "true" to also write the JSON inventory companion (below).

config.inventory — full detail on demand

With config.inventory: "true", every scheduled run also writes a stable JSON companion blob (knowledge:acme → inventory:acme, category inventory) holding the full structured data each extractor produced — the detail the prompt-baked markdown drops (e.g. every tagged AWS resource from tagging-inventory). It's byte-stable, so it only rewrites on real drift; the history table becomes an estate change log. Inspect with briar context list --prefix inventory:. Off by default.

Postgres DSN resolution order

  • config.dsn_env (if set in YAML) — explicit per-binding override
  • BRIAR_{COMPANY}_DATABASE_URL — per-company env var
  • BRIAR_DATABASE_URL — global fallback
First non-empty wins. Lets two companies on one host point at different databases.

MessageBinding

kind WRITERrequired
Writer kind. Must be a key in briar.messaging.WRITERS: jira-comment, jira-transition, github-pr-comment, bitbucket-pr-comment, slack-channel, telegram-chat.
config MAP
Per-binding config (channel overrides, env-var aliases, default statuses). Each writer decides which keys it understands — see per-writer config keys below.

GitIdentity

name STRING
git config user.name for briar agent commits.
email STRING
git config user.email.

CLI overrides win

If both the YAML git_identity and the CLI flags (--git-user-name / --git-user-email) are set, the CLI flag wins. Each field resolves independently — you can set name in YAML and override only email from the CLI.

McpServerBinding

Each key under a company's mcp: block is a handle for one MCP (Model Context Protocol) server. At agent start, briar connects, lists the server's tools, and binds each one as mcp__<handle>__<tool>. Needs the mcp extra (pip install "briar-cli[mcp]"). Two default servers (think, time) are always added unless you pass --no-default-mcp; declaring the same handle here overrides one.

transport {stdio,http}default: stdio
stdiohttp
stdio runs a local subprocess; http talks Streamable HTTP.
command STRING
Executable to start. Required for stdio.
args LIST
Arguments for command.
env MAP
Subprocess env-var name → the name of an env var in briar's own environment to copy the value from. Never put the secret itself here.
url URL
Server URL. Required for http.
headers MAP
Header name → the name of an env var holding its value (e.g. a bearer token).
tools LIST
Optional allowlist. When set, only these tool names are bound.
enabled BOOLdefault: true
false skips the server without deleting its block.
archetypes LIST
Limit the server to these agent archetypes (engineer, pr-fixer, ...). Empty = every archetype.
purpose STRING
Short "what this server is good for" text. Added to each tool's description and to the system prompt to help the agent pick the right source.

The every: cadence DSL

EveryParser turns the YAML's tiny DSL into a schedule.Job instance. Three production rules cover everything.

Grammar

<interval> "minute" | "hour" | "day" | "week" | <weekday>
<n> <interval> "10 minutes" | "4 hours" | "2 days" | "3 weeks"
<interval> at <HH:MM> "day at 03:17" | "monday at 09:00" | "hour at :15"
<interval> ::= "minute" | "hour" | "day" | "week"
<weekday> ::= "monday" | "tuesday" | "wednesday" | "thursday"
| "friday" | "saturday" | "sunday"
<n> ::= positive integer (no count allowed on weekdays)

Cadence examples

Cadence stringFires
"minute"Every minute (use for testing only — most extractors take longer than a minute).
"5 minutes"Every 5 minutes.
"hour"Every hour, at a deterministic stagger offset (see callout).
"hour at :15"Every hour at HH:15.
"4 hours"Every 4 hours.
"12 hours"Every 12 hours.
"day"Once per day, at the scheduler's start time shifted by the stagger offset.
"day at 03:17"Daily at 03:17 UTC.
"day at 07:30"Daily at 07:30 UTC. Common for nightly knowledge refresh.
"monday at 09:00"Every Monday at 09:00 UTC. Weekdays do not accept a count.
"2 weeks"Every 2 weeks, from the scheduler's start time shifted by the stagger offset.

Deterministic staggering

For every cadence without an at time (minute, N hours, day, N weeks, ...) the scheduler computes a stable offset from the SHA-1 of <company>:<task> modulo the cadence. A 1-hour job staggers 0–59 minutes, a 4-hour job staggers 0–239 minutes. Restarts land on the same offset, so there is no thundering-herd at process start. Cadences with an at time are never shifted. All at times are UTC.

How args: map to CLI flags

Anything you can pass to briar extract on the command line, you can put under args: in YAML. Two rules:

  • Replace - with _ in flag names. --pr-repo → pr_repo; --aws-extract-region → aws_extract_region.
  • Repeatable flags become YAML lists. --pr-repo a --pr-repo b → pr_repo: [a, b].
  • The shared briar extract flags work as keys too: repo, since_days, max, top_n, sample, authors_allow, authors_block, assignees_allow, assignees_block. They fill the extractor's own keys; an explicit per-extractor key (like pr_repo) wins. Note that repo also fills ticket_project / ticket_archaeology_project when those are empty.
CLI flagYAML key
--pr-repopr_repo (list)
--pr-maxpr_max (int)
--pr-authors-allowpr_authors_allow (list)
--pr-authors-blockpr_authors_block (list)
--providerprovider
--trackertracker
--cloudcloud
--aws-extract-regionaws_extract_region
--aws-extract-profileaws_extract_profile
--aws-extract-serviceaws_extract_service (list)
--ticket-projectticket_project (list)
--ticket-maxticket_max (int)
--ticket-archaeology-projectticket_archaeology_project (list)
--active-repoactive_repo (list)
--active-authors-allowactive_authors_allow (list)
--active-authors-blockactive_authors_block (list)
--deploy-repodeploy_repo (list)
--conventions-repoconventions_repo (list)
--reviewer-reporeviewer_repo (list)
--reviewer-pr-samplereviewer_pr_sample (int)
--reviewer-top-nreviewer_top_n (int)
--hotspots-repohotspots_repo (list)
--hotspots-since-dayshotspots_since_days (int)
--hotspots-max-commitshotspots_max_commits (int)
--hotspots-top-nhotspots_top_n (int)
--meetingmeeting
--meeting-since-daysmeeting_since_days (int)
--meeting-maxmeeting_max (int)
--meeting-attendee-allowmeeting_attendee_allow (list)

Per-writer config: keys

Each writer kind decides what keys its config: map understands. Three of the six ship config-aware behaviour today.

WriterConfig keyEffect
jira-comment—No config keys. Target is the ticket key; body is the comment.
jira-transitionstatusDefault transition target (e.g. "Done", "In Review"). Overridable per send via extras.status.
github-pr-comment—No config. Target is owner/repo#pr or owner/repo + extras.pr.
bitbucket-pr-comment—Same shape as the GitHub writer.
slack-channelwebhook_envOverride the env-var name holding the webhook URL. Default reads SLACK_{COMPANY}_WEBHOOK_URL. Lets a company have multiple named Slack bindings (ops, escalation) pointing at different channels.
telegram-chatchat_envOverride the env-var name holding the chat ID. Default: TELEGRAM_{COMPANY}_CHAT_ID.

More examples

Each example below is a complete, valid runbook. Copy, edit the company key, drop in real repos / project keys, and you have something runnable.

Single GitHub repo, no schedule

Cold extract a single repo on demand. No scheduler involved.

version: 1
companies:
acme:
knowledge:
store: file
name: knowledge:acme
extract:
- name: pr-archaeology
args:
pr_repo: [acme/widgets]
pr_max: 50
pr_authors_block: ["renovate[bot]", "dependabot[bot]"]
- name: code-hotspots
args:
hotspots_repo: [acme/widgets]
hotspots_since_days: 30
hotspots_top_n: 15
- name: codebase-conventions
args:
conventions_repo: [acme/widgets]
$ briar runbook extract acme.yaml

Multi-source (GitHub + Jira + AWS) with daily schedule

The mainstream stack: code in GitHub, tickets in Jira, infra in AWS. Schedule fires once a day at 03:17 UTC.

version: 1
companies:
acme:
knowledge:
store: file
name: knowledge:acme
schedules:
- task: nightly
every: "day at 03:17"
extract:
- name: pr-archaeology
args:
pr_repo: [acme/widgets, acme/api, acme/shared]
pr_max: 100
pr_authors_allow: [alice, bob, carol]
- name: active-tickets
args:
tracker: jira
ticket_project: [ACME, INFRA]
- name: ticket-archaeology
args:
tracker: jira
ticket_archaeology_project: [ACME]
ticket_max: 50
- name: aws-infra
args:
cloud: aws
aws_extract_region: us-east-1
aws_extract_service: [ecs, lambda, rds, sqs, logs]
- name: reviewer-profile
args:
reviewer_repo: [acme/widgets, acme/api]
reviewer_top_n: 5
- name: code-hotspots
args:
hotspots_repo: [acme/widgets, acme/api]
hotspots_since_days: 30

Multi-company file

Three companies in one file, each on a different cadence. briar runbook serve registers all three with deterministic staggering.

version: 1
companies:
acme:
knowledge: { store: postgres, name: knowledge:acme }
schedules:
- task: extractors
every: "day at 07:00"
extract:
- name: pr-archaeology
args: { pr_repo: [acme/widgets] }
- name: active-tickets
args: { tracker: jira, ticket_project: [ACME] }
widgets:
knowledge: { store: postgres, name: knowledge:widgets }
schedules:
- task: extractors
every: "day at 07:30"
extract:
- name: pr-archaeology
args: { pr_repo: [widgets/core] }
- name: code-hotspots
args: { hotspots_repo: [widgets/core], hotspots_since_days: 14 }
zenith:
knowledge: { store: postgres, name: knowledge:zenith }
schedules:
- task: hourly-active
every: "1 hour"
extract:
- name: active-work
args: { active_repo: [zenith/platform] }

Bitbucket Cloud + Linear (full vendor swap)

Same extractor names, different provider implementations. Knowledge goes to Postgres so the scheduler + dashboard share state without file-lock contention.

version: 1
companies:
bitspark:
knowledge:
store: postgres
name: knowledge:bitspark
config:
dsn_env: BRIAR_BITSPARK_KB_PG
messages:
pr_reply: { kind: bitbucket-pr-comment }
ops_chat: { kind: slack-channel }
escalation: { kind: telegram-chat }
schedules:
- task: archaeology
every: "monday at 02:30"
extract:
- name: pr-archaeology
args:
provider: bitbucket
pr_repo: [bitspark/platform, bitspark/api]
pr_max: 50
- name: ticket-archaeology
args:
tracker: linear
ticket_archaeology_project: [ENG]
- task: prfix
every: "hour"
extract:
- name: active-work
args:
provider: bitbucket
active_repo: [bitspark/platform]
- name: active-tickets
args:
tracker: linear
ticket_project: [ENG]
- task: extractors
every: "day at 04:17"
extract:
- name: aws-infra
args:
cloud: aws
aws_extract_region: us-west-2

GCP cloud (non-AWS provider)

The extractor is still called aws-infra (kept for back-compat) — cloud: gcp routes it onto the GCP provider. The flag names read AWS-flavoured but carry the GCP project ID / region.

version: 1
companies:
datacore:
knowledge:
store: file
name: ./knowledge/datacore/main.md
schedules:
- task: extractors
every: "day at 05:00"
extract:
- name: aws-infra
args:
cloud: gcp
aws_extract_profile: datacore-prod # GCP project ID
aws_extract_region: us-central1

Azure cloud

version: 1
companies:
fintech:
knowledge: { store: postgres, name: knowledge:fintech }
schedules:
- task: extractors
every: "day at 07:30"
extract:
- name: aws-infra
args:
cloud: azure
aws_extract_profile: 11111111-2222-3333-4444-555555555555 # subscription ID
aws_extract_region: eastus

Meeting digest with attendee filter

Only include meetings with at least one allowed attendee. meeting_attendee_allow is repeatable and folds into an OR filter.

version: 1
companies:
acme:
knowledge: { store: file, name: knowledge:acme }
schedules:
- task: meetings
every: "hour"
extract:
- name: meeting-digest
args:
meeting: fireflies
meeting_since_days: 7
meeting_max: 25
meeting_attendee_allow:

Many cadences in one company

Real-world setup: nightly heavyweights, hourly tickets, 4-hourly implementation context, weekly archaeology.

version: 1
companies:
acme:
knowledge: { store: postgres, name: knowledge:acme }
schedules:
# Nightly heavyweights — slow but complete.
- task: nightly
every: "day at 03:17"
extract:
- name: pr-archaeology
args: { pr_repo: [acme/widgets, acme/api] }
- name: reviewer-profile
args: { reviewer_repo: [acme/widgets], reviewer_top_n: 5 }
- name: code-hotspots
args: { hotspots_repo: [acme/widgets], hotspots_since_days: 30 }
# Implementation context, refreshed often.
- task: implementation
every: "4 hours"
extract:
- name: codebase-conventions
args: { conventions_repo: [acme/widgets, acme/api] }
- name: github-deployments
args: { deploy_repo: [acme/widgets] }
# Tickets, hourly.
- task: tickets
every: "hour"
extract:
- name: active-tickets
args: { tracker: jira, ticket_project: [ACME] }
# PR-fix context, hourly.
- task: prfix
every: "hour"
extract:
- name: active-work
args:
active_repo: [acme/widgets, acme/api]
active_authors_block: ["renovate[bot]", "dependabot[bot]"]
# Weekly deep-dive.
- task: weekly-archaeology
every: "monday at 02:00"
extract:
- name: ticket-archaeology
args:
tracker: jira
ticket_archaeology_project: [ACME]
ticket_max: 200

Messages — every writer kind

Wire all six writer kinds. The agent's send_message tool resolves the channel name to a writer at runtime.

version: 1
companies:
acme:
knowledge: { store: postgres, name: knowledge:acme }
messages:
# Jira write paths — distinct verbs, same auth.
ticket_comment:
kind: jira-comment
ticket_transition:
kind: jira-transition
config:
status: "In Review" # default; agent can override via extras.status
# GitHub PR comment (top-level or inline review reply).
pr_reply:
kind: github-pr-comment
# Bitbucket equivalent.
bb_pr_reply:
kind: bitbucket-pr-comment
# Slack — default channel via SLACK_ACME_WEBHOOK_URL.
ops_chat:
kind: slack-channel
# Slack — second channel via per-binding env-var override.
escalation_slack:
kind: slack-channel
config:
webhook_env: SLACK_ACME_ESCALATION_WEBHOOK_URL
# Telegram — alt-channel override.
escalation_telegram:
kind: telegram-chat
config:
chat_env: TELEGRAM_ACME_ALT_CHAT

Jira session-cookie auth (SSO tenants)

Some Atlassian tenants disable API tokens and require session-cookie auth. Set the session env vars and Briar auto-detects which auth mode to use.

version: 1
companies:
acme:
knowledge: { store: file, name: knowledge:acme }
schedules:
- task: tickets
every: "hour"
extract:
- name: active-tickets
args:
tracker: jira
ticket_project: [ACME]
# Env vars (set out-of-band, e.g. in secrets.env):
# JIRA_ACME_URL = https://acme.atlassian.net
# JIRA_ACME_TENANT_SESSION_TOKEN = <tenant.session.token cookie value>
# JIRA_ACME_XSRF_TOKEN = <atlassian.xsrf.token cookie value>
#
# Optional, force the auth picker:
# JIRA_ACME_AUTH_KIND=session

Staging vs production via profile

Use the free-form profile: field as a marker, and put staging on a slower cadence with a separate knowledge blob.

version: 1
companies:
acme-prod:
profile: production
knowledge: { store: postgres, name: knowledge:acme-prod }
schedules:
- task: extractors
every: "day at 03:17"
extract:
- name: pr-archaeology
args: { pr_repo: [acme/widgets] }
acme-staging:
profile: staging
knowledge: { store: postgres, name: knowledge:acme-staging }
schedules:
- task: extractors
every: "day at 04:00"
extract:
- name: pr-archaeology
args: { pr_repo: [acme/widgets], pr_max: 25 }

Cookbook patterns

Share extractor args via anchors

YAML anchors and aliases let you avoid repeating long arg blocks across schedules.

version: 1
companies:
acme:
knowledge: { store: file, name: knowledge:acme }
schedules:
- task: nightly
every: "day at 03:17"
extract:
- name: pr-archaeology
args: &pr_args
pr_repo: [acme/widgets, acme/api]
pr_max: 100
pr_authors_block: ["renovate[bot]", "dependabot[bot]"]
- task: backfill
every: "monday at 02:00"
extract:
- name: pr-archaeology
args:
<<: *pr_args
pr_max: 500 # override just this key

Run one task on demand

Pass --task to briar runbook extract to fire one named schedule without waiting for its cadence.

$ briar runbook extract runbooks/acme.yaml --task tickets
$ briar runbook extract runbooks/acme.yaml --task nightly

Avoid thundering-herd at startup

You don't need to. Briar staggers every job without an at time deterministically using SHA-1 of <company>:<task>. A 1-hour job for acme:tickets and widgets:tickets land at different minute offsets. Restarts hit the same offset, so the schedule looks stable across restarts.

Per-company Postgres databases

Three resolution layers — first non-empty wins.

version: 1
companies:
# Uses the explicit binding override.
acme:
knowledge:
store: postgres
name: knowledge:acme
config:
dsn_env: BRIAR_ACME_KB_PG # 1️⃣ wins
# Falls back to the per-company env var (auto-derived).
widgets:
knowledge:
store: postgres
name: knowledge:widgets # reads BRIAR_WIDGETS_DATABASE_URL 2️⃣
# No company override → uses BRIAR_DATABASE_URL global.
zenith:
knowledge:
store: postgres
name: knowledge:zenith # reads BRIAR_DATABASE_URL 3️⃣

Audit credential coverage before running

$ briar secrets doctor --examples runbooks/
# Walks every (company × extractor × provider) and (company × messages × writer)
# tuple. Names the missing env vars. Doesn't print values.

Turn a CLI command into YAML

Take the command line, strip briar extract --company X, and put the remaining flags under args: after renaming - to _. Repeatable flags become lists.

# CLI form
briar extract --company acme \
--include pr-archaeology \
--pr-repo acme/widgets --pr-repo acme/api \
--pr-max 50 \
--pr-authors-block "renovate[bot]"
# YAML form
companies:
acme:
knowledge: { store: file, name: knowledge:acme }
extract:
- name: pr-archaeology
args:
pr_repo: [acme/widgets, acme/api]
pr_max: 50
pr_authors_block: ["renovate[bot]"]

Validating a runbook

Three increasingly thorough checks.

1. Parse-only

$ python -c "
$ from pathlib import Path
$ from briar.iac.runbook.executor import RunbookLoader
$ rb = RunbookLoader.load(Path('runbooks/acme.yaml'))
$ print(f'companies: {sorted(rb.companies.keys())}')
$ "

Bad YAML or unknown extractor names raise a locator-aware Pydantic error pointing at the line and field.

2. Credential audit

$ briar secrets doctor --examples runbooks/

Reports every missing env var by extractor and writer.

3. Dry-run a single task

$ briar runbook extract runbooks/acme.yaml --task nightly

Runs once, end-to-end. The result is written to the configured knowledge store; nothing is scheduled.

Roundtrip-safe

Briar's loader uses Pydantic with strict mode on the outer shape and forgiving mode on the company entry — legacy fields like profile:, workspace_id:, and knowledge_file: still load even if you stop using them, so you can move forward without breaking old YAMLs you keep around for reference.

See also

  • briar runbook — the subcommands that consume these YAMLs.
  • briar extract — the imperative form of every args: key.
  • Plugin registries — every extractor / writer / provider name you can use.
  • Configuration — per-company env var naming convention and credential stores.
  • Recipes — full multi-host deployment walkthroughs.