Reference
Runbook YAML schema.
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
- Full-shape example
- Top-level keys · CompanyEntry · ExtractEntry · ScheduleEntry
- KnowledgeBinding · MessageBinding · GitIdentity
- The
every:cadence DSL - How
args:map to CLI flags - Per-writer
config:keys - More examples — single-source, multi-source, vendor swaps, postgres, Linear, GitHub Projects v2, Bitbucket, meeting filters
- Cookbook patterns
- Validating a runbook
Minimal example
The smallest useful runbook — one company, one extractor, no schedule.
version: 1companies:acme:knowledge:store: filename: knowledge:acmeextract:- name: pr-archaeologyargs:pr_repo: [acme/widgets]- name: active-ticketsargs:tracker: jiraticket_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: 1companies:acme:profile: productionknowledge:store: postgresname: knowledge:acmeconfig:dsn_env: BRIAR_ACME_DATABASE_URLgit_identity:name: Briar Agentmessages:ticket_comment: { kind: jira-comment }ops_chat: { kind: slack-channel }escalation:kind: telegram-chatconfig:chat_env: TELEGRAM_ACME_ALT_CHATextract:- name: pr-archaeologyargs:provider: githubpr_repo: [acme/widgets, acme/api]pr_max: 100- name: aws-infraargs:cloud: awsaws_extract_region: us-east-1aws_extract_service: [ecs, lambda, rds]schedules:- task: extractorsevery: "day at 07:30"extract:- name: pr-archaeology- name: meeting-digest- task: hourly-ticketsevery: "1 hour"extract:- name: active-tickets
Top-level keys
version INTdefault: 11 ships today.companies MAPCompanyEntry. The company key is the token used in env-var interpolation — --company acme matches companies.acme and GITHUB_ACME_TOKEN.CompanyEntry
profile STRINGworkspace_id STRINGknowledge_file PATHknowledge: block below.knowledge KnowledgeBindingKnowledgeBinding below.extract LIST[ExtractEntry]briar runbook extract when no --task is passed.schedules LIST[ScheduleEntry]briar runbook serve.messages MAP[name, MessageBinding]briar agent's send_message tool resolves names to writers at runtime.git_identity GitIdentitybriar agent worktree commits.ExtractEntry
name EXTRACTORrequiredbriar.extract.EXTRACTORS — see the registries page.args MAP- replaced by _ (e.g. pr-repo → pr_repo). See the mapping table below.ScheduleEntry
task STRINGrequiredbriar runbook extract --task to fire one schedule on demand.every STRINGrequiredEveryParser. See the full grammar in the cadence DSL section.extract LIST[ExtractEntry]KnowledgeBinding
store {file,postgres}default: filefilepostgresname BLOB_NAMEknowledge:<company>.root PATHconfig MAPdsn_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 overrideBRIAR_{COMPANY}_DATABASE_URL— per-company env varBRIAR_DATABASE_URL— global fallback
MessageBinding
kind WRITERrequiredbriar.messaging.WRITERS: jira-comment, jira-transition, github-pr-comment, bitbucket-pr-comment, slack-channel, telegram-chat.config MAPGitIdentity
name STRINGgit config user.name for briar agent commits.email STRINGgit config user.email.CLI overrides win
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.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 string | Fires |
|---|---|
"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 hour. |
"day at 03:17" | Daily at 03:17 wall-clock. |
"day at 07:30" | Daily at 07:30 — common for nightly knowledge refresh. |
"monday at 09:00" | Every Monday at 09:00. Weekdays do not accept a count. |
"2 weeks" | Every 2 weeks at the scheduler's start time. |
Deterministic staggering
minute, hour, N minutes, N hours) 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 — no thundering-herd at process start.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].
| CLI flag | YAML key |
|---|---|
--pr-repo | pr_repo (list) |
--pr-max | pr_max (int) |
--pr-authors-allow | pr_authors_allow (list) |
--pr-authors-block | pr_authors_block (list) |
--provider | provider |
--tracker | tracker |
--cloud | cloud |
--aws-extract-region | aws_extract_region |
--aws-extract-profile | aws_extract_profile |
--aws-extract-service | aws_extract_service (list) |
--ticket-project | ticket_project (list) |
--ticket-max | ticket_max (int) |
--ticket-archaeology-project | ticket_archaeology_project (list) |
--active-repo | active_repo (list) |
--active-authors-allow | active_authors_allow (list) |
--active-authors-block | active_authors_block (list) |
--deploy-repo | deploy_repo (list) |
--conventions-repo | conventions_repo (list) |
--reviewer-repo | reviewer_repo (list) |
--reviewer-pr-sample | reviewer_pr_sample (int) |
--reviewer-top-n | reviewer_top_n (int) |
--hotspots-repo | hotspots_repo (list) |
--hotspots-since-days | hotspots_since_days (int) |
--hotspots-max-commits | hotspots_max_commits (int) |
--hotspots-top-n | hotspots_top_n (int) |
--meeting | meeting |
--meeting-since-days | meeting_since_days (int) |
--meeting-max | meeting_max (int) |
--meeting-attendee-allow | meeting_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.
| Writer | Config key | Effect |
|---|---|---|
jira-comment | — | No config keys. Target is the ticket key; body is the comment. |
jira-transition | status | Default 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-channel | webhook_env | Override 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-chat | chat_env | Override 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: 1companies:acme:knowledge:store: filename: knowledge:acmeextract:- name: pr-archaeologyargs:pr_repo: [acme/widgets]pr_max: 50pr_authors_block: ["renovate[bot]", "dependabot[bot]"]- name: code-hotspotsargs:hotspots_repo: [acme/widgets]hotspots_since_days: 30hotspots_top_n: 15- name: codebase-conventionsargs: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.
version: 1companies:acme:knowledge:store: filename: knowledge:acmeschedules:- task: nightlyevery: "day at 03:17"extract:- name: pr-archaeologyargs:pr_repo: [acme/widgets, acme/api, acme/shared]pr_max: 100pr_authors_allow: [alice, bob, carol]- name: active-ticketsargs:tracker: jiraticket_project: [ACME, INFRA]- name: ticket-archaeologyargs:tracker: jiraticket_archaeology_project: [ACME]ticket_max: 50- name: aws-infraargs:cloud: awsaws_extract_region: us-east-1aws_extract_service: [ecs, lambda, rds, sqs, logs]- name: reviewer-profileargs:reviewer_repo: [acme/widgets, acme/api]reviewer_top_n: 5- name: code-hotspotsargs: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: 1companies:acme:knowledge: { store: postgres, name: knowledge:acme }schedules:- task: extractorsevery: "day at 07:00"extract:- name: pr-archaeologyargs: { pr_repo: [acme/widgets] }- name: active-ticketsargs: { tracker: jira, ticket_project: [ACME] }widgets:knowledge: { store: postgres, name: knowledge:widgets }schedules:- task: extractorsevery: "day at 07:30"extract:- name: pr-archaeologyargs: { pr_repo: [widgets/core] }- name: code-hotspotsargs: { hotspots_repo: [widgets/core], hotspots_since_days: 14 }zenith:knowledge: { store: postgres, name: knowledge:zenith }schedules:- task: hourly-activeevery: "1 hour"extract:- name: active-workargs: { 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: 1companies:bitspark:knowledge:store: postgresname: knowledge:bitsparkconfig:dsn_env: BRIAR_BITSPARK_KB_PGmessages:pr_reply: { kind: bitbucket-pr-comment }ops_chat: { kind: slack-channel }escalation: { kind: telegram-chat }schedules:- task: archaeologyevery: "monday at 02:30"extract:- name: pr-archaeologyargs:provider: bitbucketpr_repo: [bitspark/platform, bitspark/api]pr_max: 50- name: ticket-archaeologyargs:tracker: linearticket_archaeology_project: [ENG]- task: prfixevery: "hour"extract:- name: active-workargs:provider: bitbucketactive_repo: [bitspark/platform]- name: active-ticketsargs:tracker: linearticket_project: [ENG]- task: extractorsevery: "day at 04:17"extract:- name: aws-infraargs:cloud: awsaws_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: 1companies:datacore:knowledge:store: filename: ./knowledge/datacore/main.mdschedules:- task: extractorsevery: "day at 05:00"extract:- name: aws-infraargs:cloud: gcpaws_extract_profile: datacore-prod # GCP project IDaws_extract_region: us-central1
Azure cloud
version: 1companies:fintech:knowledge: { store: postgres, name: knowledge:fintech }schedules:- task: extractorsevery: "day at 07:30"extract:- name: aws-infraargs:cloud: azureaws_extract_profile: 11111111-2222-3333-4444-555555555555 # subscription IDaws_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: 1companies:acme:knowledge: { store: file, name: knowledge:acme }schedules:- task: meetingsevery: "hour"extract:- name: meeting-digestargs:meeting: firefliesmeeting_since_days: 7meeting_max: 25meeting_attendee_allow:
Many cadences in one company
Real-world setup: nightly heavyweights, hourly tickets, 4-hourly implementation context, weekly archaeology.
version: 1companies:acme:knowledge: { store: postgres, name: knowledge:acme }schedules:# Nightly heavyweights — slow but complete.- task: nightlyevery: "day at 03:17"extract:- name: pr-archaeologyargs: { pr_repo: [acme/widgets, acme/api] }- name: reviewer-profileargs: { reviewer_repo: [acme/widgets], reviewer_top_n: 5 }- name: code-hotspotsargs: { hotspots_repo: [acme/widgets], hotspots_since_days: 30 }# Implementation context, refreshed often.- task: implementationevery: "4 hours"extract:- name: codebase-conventionsargs: { conventions_repo: [acme/widgets, acme/api] }- name: github-deploymentsargs: { deploy_repo: [acme/widgets] }# Tickets, hourly.- task: ticketsevery: "hour"extract:- name: active-ticketsargs: { tracker: jira, ticket_project: [ACME] }# PR-fix context, hourly.- task: prfixevery: "hour"extract:- name: active-workargs:active_repo: [acme/widgets, acme/api]active_authors_block: ["renovate[bot]", "dependabot[bot]"]# Weekly deep-dive.- task: weekly-archaeologyevery: "monday at 02:00"extract:- name: ticket-archaeologyargs:tracker: jiraticket_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: 1companies:acme:knowledge: { store: postgres, name: knowledge:acme }messages:# Jira write paths — distinct verbs, same auth.ticket_comment:kind: jira-commentticket_transition:kind: jira-transitionconfig: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-channelconfig:webhook_env: SLACK_ACME_ESCALATION_WEBHOOK_URL# Telegram — alt-channel override.escalation_telegram:kind: telegram-chatconfig: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: 1companies:acme:knowledge: { store: file, name: knowledge:acme }schedules:- task: ticketsevery: "hour"extract:- name: active-ticketsargs:tracker: jiraticket_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: 1companies:acme-prod:profile: productionknowledge: { store: postgres, name: knowledge:acme-prod }schedules:- task: extractorsevery: "day at 03:17"extract:- name: pr-archaeologyargs: { pr_repo: [acme/widgets] }acme-staging:profile: stagingknowledge: { store: postgres, name: knowledge:acme-staging }schedules:- task: extractorsevery: "day at 04:00"extract:- name: pr-archaeologyargs: { 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: 1companies:acme:knowledge: { store: file, name: knowledge:acme }schedules:- task: nightlyevery: "day at 03:17"extract:- name: pr-archaeologyargs: &pr_argspr_repo: [acme/widgets, acme/api]pr_max: 100pr_authors_block: ["renovate[bot]", "dependabot[bot]"]- task: backfillevery: "monday at 02:00"extract:- name: pr-archaeologyargs:<<: *pr_argspr_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 sub-day jobs 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: 1companies:# Uses the explicit binding override.acme:knowledge:store: postgresname: knowledge:acmeconfig:dsn_env: BRIAR_ACME_KB_PG # 1️⃣ wins# Falls back to the per-company env var (auto-derived).widgets:knowledge:store: postgresname: knowledge:widgets # reads BRIAR_WIDGETS_DATABASE_URL 2️⃣# No company override → uses BRIAR_DATABASE_URL global.zenith:knowledge:store: postgresname: 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 formbriar extract --company acme \--include pr-archaeology \--pr-repo acme/widgets --pr-repo acme/api \--pr-max 50 \--pr-authors-block "renovate[bot]"# YAML formcompanies:acme:knowledge: { store: file, name: knowledge:acme }extract:- name: pr-archaeologyargs:pr_repo: [acme/widgets, acme/api]pr_max: 50pr_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
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.