How FFC Automation Works
Free For Charity runs its infrastructure β Cloudflare DNS, WHMCS billing, Microsoft 365, Zeffy donations, Google Analytics, and the GitHub sites themselves β through 112 auditable GitHub Actions workflows. This page is the living catalog: what each workflow does, how they are numbered and named, and the safety model that protects every run. It is generated from the workflows themselves, so it cannot drift.
The numbering convention β and why
Every workflow has a 3-digit number whose first digit is the system the workflow targets. A new admin (or an AI agent) can tell from the number alone which external API a workflow drives, and new workflows can never collide β each category has its own numbering space.
Naming: workflows are named NNN. Target - Description [TAG] and their files are NNN-slug.yml. The [TAG] lists every API the workflow calls, joined with + (for example [CF+M365]).
What counts as βan APIβ: the API the workflow actually calls β not the service the records are for (creating Microsoft 365 DNS records via the Cloudflare API is [CF]), and not plumbing (fetching credentials from Azure Key Vault, or posting a result comment to GitHub, never counts).
Multi-system workflows keep one number from the system that owns the deliverable (writes beat reads). They exist as single workflows only when they are aggregators (a report that joins several sources) or transactions (two writes that must succeed together to avoid a half-configured state).
The safety model
A live change generally has to get past several independent layers, not just one:
- Read vs. write credentials. Read-only workflows load reader-scope credentials and cannot mutate anything; write workflows load writer-scope credentials.
- Environment approval gates. Write environments (and some sensitive read environments) require a human reviewer to approve the run before it proceeds.
dry_rundefaults to preview. Write workflows preview what they would change; going live requires explicitly passingdry_run=false.- Typed confirmation for the highest-stakes actions (for example, domain registration requires retyping the exact domain).
- No stored secrets. Credentials are fetched from Azure Key Vault at run time via OIDC and masked line-by-line; nothing sensitive lives in the repository.
The complete catalog
Generated from the workflow files in FreeForCharity/FFC-Cloudflare-Automation. Safety levels: Reads = no external mutation; Writes (dry-run default) = mutates only with dry_run=false; Writes (gated) = mutates when run, protected by an approval gate.
Cloudflare / DNS / Domain
26 workflows| # | Workflow | APIs | Safety | Runs on |
|---|---|---|---|---|
| 101 | Domain - Status (All Sources) 101-domain-status.yml | CFM365 | Reads M365 job reads FFC-tenant status; waits on m365-prod approval | workflow_dispatch |
| 102 | Domain - Add to FFC Cloudflare + WHMCS Nameservers (Admin) 102-domain-add-ffc-cloudflare-and-whmcs.yml | CFWHMCS | Writes (gated) β | workflow_dispatch |
| 103 | Domain - Enforce Standard (GitHub Apex + M365) 103-enforce-domain-standard.yml | CFM365 | Writes (dry-run default) `dry_run` (default true); exo jobs act on FFC tenant β `skip_m365=true` for external-tenant charities | workflow_dispatch |
| 104 | Domain - Export Inventory (All Sources) 104-domain-export-inventory.yml | CFM365WHMCSWPMUDEV | Reads waits on m365-prod and wpmudev-prod approvals (WHMCS job ungated) | workflow_dispatch |
| 105 | DNS - Manage Record (Manual / Issue Label) 105-manage-record.yml | CF | Writes (dry-run default) `dry_run` (default true); issue-label trig | issues, workflow_dispatch |
| 106 | DNS - Enforce Standard (DNS-only) 106-enforce-standard.yml | CF | Writes (dry-run default) `dry_run` (default true); `mail_provider` default `from-registry`; UNLISTED domains have mail untouched; a listed domain is on EXACTLY ONE provider (enforcing REMOVES the other's MX/CNAME/SRV); MOVING providers needs `confirm_mail_change` = the domain name | workflow_dispatch |
| 107 | DNS - Audit Compliance (Report) 107-audit-compliance.yml | CF | Reads β | workflow_dispatch |
| 108 | DNS - Export Cloudflare Zones (Report) 108-export-summary.yml | CF | Reads β | workflow_dispatch |
| 109 | DNS - Export All Records (Full, per-record) 109-dns-export-all-records.yml | CF | Reads β | workflow_dispatch |
| 110 | DNS - Create Zone (Admin) 110-cloudflare-zone-create.yml | CF | Writes (gated) β | workflow_dispatch |
| 111 | DNS - Create Redirect Rule (Admin) 111-dns-create-redirect-rule.yml | CF | Writes (dry-run default) `dry_run` (default true) | workflow_dispatch |
| 112 | DNS - Bulk Replace A-record IP (All Zones) 112-dns-bulk-replace-a-ip.yml | CF | Writes (gated) high blast radius; serialized | workflow_dispatch |
| 113 | Domain - Registrar Search / Check / Register (Admin, DRAFT) 113-cloudflare-domain-register.yml | CF | Writes (gated) `mode` (default `check`); register needs `mode=execute-register` + `confirm_domain` | issues, workflow_dispatch |
| 114 | Domain - Validate Cloudflare Registrar API Access (Read-only) 114-cloudflare-registrar-access-check.yml | CF | Reads never charges; but `cloudflare-tokens-from-kv` runs with **`scope: write`** β loads `wr-all-*` CF tokens, so **not auto-approvable** (#915) | workflow_dispatch |
| 115 | Domain - Transfer Readiness Preflight (Report) 115-domain-transfer-preflight.yml | WHMCS | Reads β | workflow_dispatch |
| 116 | Domain - Transfer EPP/Auth Code Probe (Admin) 116-domain-transfer-epp-probe.yml | WHMCS | Writes (dry-run default) `dry-run` vs `execute` | workflow_dispatch |
| 117 | Domain - Post-Transfer Verification (Report) 117-domain-transfer-verify.yml | CF | Reads β | workflow_dispatch |
| 118 | Domain - Registrar Lock / Unlock 118-whmcs-domain-lock.yml | WHMCS | Writes (dry-run default) `dry_run` (default true) | workflow_dispatch |
| 119 | DNS - Bulk Staging CNAME -> GitHub Pages (FFC-EX) 119-bulk-staging-cname-github-pages.yml | CF | Writes (dry-run default) `dry_run` (default true); serialized | workflow_dispatch |
| 120 | DNS + GH Pages - Bulk Cutover staging -> Apex (FFC-EX) 120-bulk-cutover-to-github-pages.yml | CFGH | Writes (dry-run default) `dry_run` (default true); serialized | workflow_dispatch |
| 121 | DNS + GH Pages - Fleet Cutover Preflight (FFC-EX) 121-fleet-cutover-preflight.yml | Reads read-only DoH/HTTPS probes; go/no-go for 120 | workflow_dispatch | |
| 122 | Cloudflare - Zone Member Add (Domain Admin) 122-cloudflare-zone-member-add.yml | CF | Writes (dry-run default) `dry_run` (default true); preview job always runs read-only; idempotent (existing member = no change) | workflow_dispatch |
| 123 | Domain - Inbound Transfer Preflight (Report) 123-domain-inbound-transfer-preflight.yml | Reads read-only RDAP + offline analysis; classifies foreign-registrar domains for the eNom/WHMCS inbound path | workflow_dispatch | |
| 124 | Cloudflare - Cache Purge (Admin) 124-cloudflare-cache-purge.yml | CF | Writes (dry-run default) `dry_run` (default true); preview job always runs read-only; serialized per zone; `purge_everything` refetches the whole zone from origin β prefer explicit `urls` | workflow_dispatch |
| 125 | Cloudflare - Cache Rules Audit (Read-only) 125-cloudflare-cache-rules-audit.yml | CF | Reads read-only; reports whether 4xx/5xx can be edge-cached; `fail_on_finding` (default false) turns warn-level findings into a non-zero exit | workflow_dispatch |
| 126 | Domain - Redirect Forensics (Read-only) 126-domain-redirect-forensics.yml | CF | Reads read-only; walks every HTTP hop manually (never follows redirects) and reads DNS, dynamic-redirect rulesets and legacy Page Rules; prints to the job log, never an artifact, so an egress-restricted caller can read it | workflow_dispatch |
WHMCS
31 workflows| # | Workflow | APIs | Safety | Runs on |
|---|---|---|---|---|
| 201 | WHMCS - Export Domains (Report) 201-whmcs-export-domains.yml | WHMCS | Reads ungated read lane; artifacts are export catalogs | workflow_dispatch |
| 202 | WHMCS - Export Products (Report) 202-whmcs-export-products.yml | WHMCS | Reads ungated read lane; artifacts are export catalogs | workflow_dispatch |
| 203 | WHMCS - Export Payment Methods (Research) 203-whmcs-export-payment-methods.yml | WHMCS | Reads ungated read lane; artifacts are export catalogs | workflow_dispatch |
| 204 | WHMCS - Charity Onboard (client + contacts + order) 204-whmcs-charity-onboard.yml | WHMCS | Writes (dry-run default) `dry_run` (default true); idempotent | workflow_dispatch |
| 205 | WHMCS - Open Ticket (manual) 205-whmcs-ticket-open.yml | WHMCS | Writes (gated) one-way GitHubβWHMCS | workflow_dispatch |
| 206 | WHMCS - Issue to Ticket (one-way) 206-whmcs-issue-to-ticket.yml | WHMCS | Writes (gated) one-way GitHubβWHMCS | issues |
| 207 | WHMCS - Ticket Respond (templated) 207-whmcs-ticket-respond.yml | WHMCS | Writes (dry-run default) `dry_run` (default true); live reply needs admin username | workflow_dispatch |
| 208 | WHMCS - Export Tickets (Report) 208-whmcs-tickets-export.yml | WHMCS | Reads β | workflow_dispatch |
| 209 | WHMCS - Tickets Triage (Open/Customer-Reply) 209-whmcs-tickets-triage.yml | WHMCS | Reads summary masks PII | schedule, workflow_dispatch |
| 210 | WHMCS - Orders Triage (Pending/Fraud/Active) 210-whmcs-orders-triage.yml | WHMCS | Reads summary masks PII | schedule, workflow_dispatch |
| 211 | WHMCS - Order Update (accept/cancel/fraud) 211-whmcs-order-update.yml | WHMCS | Writes (dry-run default) `dry_run` (default true); one order at a time | workflow_dispatch |
| 212 | WHMCS - Product Add (catalog) 212-whmcs-product-add.yml | WHMCS | Writes (dry-run default) `dry_run` (default true); idempotent | workflow_dispatch |
| 213 | WHMCS -> Zeffy Payments Import (Draft) 213-whmcs-zeffy-payments-import-draft.yml | WHMCS | Reads (builds a file) output is a draft; serialized | workflow_dispatch |
| 214 | WHMCS - Clients Metrics (aggregate, no PII) 214-whmcs-clients-metrics.yml | WHMCS | Reads aggregate counts only β no PII in artifact or summary | workflow_dispatch |
| 215 | WHMCS - Nonprofit Clients Metrics (service evidence, no PII) 215-whmcs-nonprofit-clients-metrics.yml | WHMCS | Reads aggregate counts + product names only (no PII) | workflow_dispatch |
| 216 | WHMCS - Activity Metrics (full history, no PII) 216-whmcs-activity-metrics.yml | WHMCS | Reads aggregate counts only (no PII) | workflow_dispatch |
| 217 | WHMCS - Client Fields Survey (aggregate, no PII) 217-whmcs-client-fields-survey.yml | WHMCS | Reads throttled per-client loop; vocabulary-matched values only (no PII) | workflow_dispatch |
| 218 | WHMCS - Sites-List Reconciliation + Product Alignment 218-whmcs-siteslist-reconciliation.yml | WHMCS | Reads aggregate summary; artifact carries org-domain gap lists only | workflow_dispatch |
| 219 | WHMCS - Application Detail (Client + Orders, PII-masked) 219-whmcs-application-detail.yml | WHMCS | Reads single-client projection; personal contact fields always masked | workflow_dispatch |
| 220 | WHMCS - Served-Per-Year Metrics (span evidence, no PII) 220-whmcs-served-metrics.yml | WHMCS | Reads aggregate counts only (no PII) | workflow_dispatch |
| 221 | WHMCS - Application Search (find by domain/org) 221-whmcs-application-search.yml | WHMCS | Reads GetClientsProducts sweep; personal contact fields masked; **ungated** β `whmcs-secrets-from-kv` with `scope: read` on the `READ_ALL_FFC_AZURE_*` (ffc-admin-kv-reader) identity β `read-all-ffc-whmcs-*` (#920; was β
`whmcs-prod` on the writer identity, held under #915) | workflow_dispatch |
| 222 | WHMCS - Cloudflare Registrar Product Alignment 222-whmcs-product-alignment.yml | WHMCSCF | Writes (dry-run default) dry_run default true; $0 AddOrder of pid 39 onto registrar-domain clients; idempotent | workflow_dispatch |
| 223 | WHMCS - Import Cloudflare Registrar Domains as Domain Records 223-whmcs-import-cloudflare-domains.yml | WHMCSCF | Writes (dry-run default) dry_run default true; creates/populates domain records (registrar None) from CF facts | workflow_dispatch |
| 224 | WHMCS - GitHub Pages Product Alignment 224-whmcs-github-pages-product-alignment.yml | WHMCSGH | Writes (dry-run default) dry_run default true; $0 AddOrder of pid 40 onto FFC-EX GitHub-Pages-site clients; idempotent | workflow_dispatch |
| 225 | WHMCS - Domain Order URL Verify 225-whmcs-domain-order-url-verify.yml | WHMCS | Reads report-only; GETs charity-supplied URLs; never accepts/cancels an order | schedule, workflow_dispatch |
| 226 | WHMCS - Application Triage (rank + accept onboarding orders) 226-whmcs-application-triage.yml | WHMCS | Writes (report default) report + reconcile-report read-only; approve accepts ONLY listed $0 orderids; reconcile cancels ONLY listed orderids + emails those clients a re-file email (email-first, no cancel-all) | workflow_dispatch |
| 227 | cPanel - Deploy WHMCS Hooks (FTPS/lftp) 227-whmcs-hooks-deploy.yml | cPanel | Writes (verify default) mode=verify (read-only) vs deploy; hooks input is basename-validated | workflow_dispatch |
| 228 | WHMCS - Fraud Review (FraudLabs Pro) 228-whmcs-fraud-review.yml | FRAUDLABSWHMCS | Writes (issues only) read-only external APIs; masks PII; recommends only (never clears β clearing is via gated 211); scaffold inert until the FraudLabs Pro key + fraudlabspro-prod-read env are provisioned; daily weekday schedule upserts one rolling GitHub tracking issue (issues:write, ambient token) that self-closes when the Fraud queue clears | schedule, workflow_dispatch |
| 229 | WHMCS - Populate Client Fields from Product Answers 229-whmcs-client-field-populate.yml | WHMCS | Writes (dry-run default) `dry_run` (default true); copies one client's product custom-field answers into their Custom Client Fields via UpdateClient; idempotent + non-destructive (existing values preserved unless `overwrite=true`); name map in `config/whmcs-client-field-populate-map.json` β verify the unmapped list on dry-run before live | workflow_dispatch |
| 230 | WHMCS - Record Field Set (client/contact/service/domain) 230-whmcs-record-field-set.yml | WHMCS | Writes (record field) `dry_run` (default true); one record + one field per dispatch; strict per-target allowlist of writable fields; refuses to replace a different existing value without `force`; reports `previousValue` | workflow_dispatch |
| 231 | WHMCS - Domain Order Add (Register/Transfer) (Admin) 231-whmcs-domain-order-add.yml | WHMCS | Writes (dry-run default) `mode` (default `dry-run`) previews AddOrder and writes nothing; `execute` places a real order; pid fixed by `order_type` (41 transfer / 39 register); underlying script skips when the client already has a non-terminated service for the product unless `allow_duplicate`; `no_email` defaults true so a first live run cannot surprise a charity; price forced to 0 | workflow_dispatch |
Microsoft (M365 / Azure / Graph)
8 workflows| # | Workflow | APIs | Safety | Runs on |
|---|---|---|---|---|
| 301 | M365 (FFC Tenant) - Domain Preflight (Read-only) 301-m365-domain-preflight.yml | M365CF | Reads M365 job waits on m365-prod approval | workflow_dispatch |
| 302 | M365 (FFC Tenant) - List Tenant Domains 302-m365-list-domains.yml | M365 | Reads waits on m365-prod approval | workflow_dispatch |
| 303 | M365 (FFC Tenant) - Domain Status + DKIM (Toolbox) 303-m365-domain-and-dkim.yml | M365 | Reads read-oriented toolbox; waits on m365-prod approval | workflow_dispatch |
| 304 | M365 (FFC Tenant) - Enable DKIM (Exchange Online) 304-m365-dkim-enable.yml | M365CF | Writes (gated) β | workflow_dispatch |
| 305 | M365 (FFC Tenant) - Add Tenant Domain (INTERNAL ONLY) 305-m365-add-tenant-domain.yml | M365 | Writes (dry-run default) `dry_run` (default true); also gated by m365-prod approval | workflow_dispatch |
| 306 | Discover - Uncaptured Comms (FFC Tenant M365, PII masked) 306-discover-uncaptured-comms.yml | M365 | Reads PII masked; dispatch-only; org mailboxes only; waits on m365-prod approval | workflow_dispatch |
| 320 | Azure - Key Vault Secret Inventory (audit) 320-azure-kv-secret-inventory.yml | MS | Reads values never printed; placeholder/stale flags only | schedule, workflow_dispatch |
| 321 | Azure - KV Credential Liveness + Expiry Monitor 321-azure-kv-credential-liveness.yml | MSGHCF | Reads daily two-signal credential health check: expiry/enabled for every vault secret from the attribute listing (values never read), plus a read-only liveness probe (`GET /user`, Cloudflare `GET /zones`) for the four `read-all-*` credentials that can verify themselves β the library refuses a `wr-all-*` probe target before anything is read, so write-scope material is expiry-monitored without ever being fetched; a probe that cannot be evaluated is reported as unverifiable, never as passing; values never printed or persisted; rolling issue upsert/close; ungated by design (a credential monitor must not wait on a human) | schedule, workflow_dispatch |
Zeffy
3 workflows| # | Workflow | APIs | Safety | Runs on |
|---|---|---|---|---|
| 401 | Zeffy - Campaigns Export 401-zeffy-campaigns-export.yml | ZEFFY | Reads (+ PR delivery) PII masked; the published `docs/data/ffc-zeffy-campaigns.json` is title/url/status only (no financials); `deliver` opens a reviewable data PR via CBM_TOKEN; loads `wr-all-ffc-zeffy-api-key` (default `write`) **and** `wr-all-cbm-github-pat` β **not auto-approvable** (#915; on the β
list 2026-07-23 β 2026-07-29) | workflow_dispatch |
| 402 | Zeffy - Payments Export (PII masked) 402-zeffy-payments-export.yml | ZEFFY | Reads PII masked; never `-IncludePii` | workflow_dispatch |
| 403 | Zeffy - Contacts Export (PII masked) 403-zeffy-contacts-export.yml | ZEFFY | Reads PII masked; never `-IncludePii` | workflow_dispatch |
| # | Workflow | APIs | Safety | Runs on |
|---|---|---|---|---|
| 501 | Google - API Smoke (GA4 connectivity) 501-google-api-smoke.yml | GOOGLE | Reads read-only; fails closed; reusable via `workflow_call` | workflow_call, workflow_dispatch |
| 502 | Google - Analytics Report (GA4 -> JSON) 502-google-analytics-report.yml | GOOGLE | Reads delivers JSON to ffcadmin via PR (reads on `read-all-cbm-ffc-copilot-mcp-github-pat`, push + PR on the 2-repo `wr-all-cbm-datasync-github-pat`, both from KV; ungated lane #834, writer ruling #848); PII-safe aggregates. Same PR also syncs the workflow catalog and the Agentic OS status feed (`agentic-os-status.json`, generated by `scripts/generate-agentic-os-status.py`, REST-only) | schedule, workflow_dispatch |
| 503 | Google - GTM Provision (per-charity container) 503-google-gtm-provision.yml | GOOGLE | Writes (dry-run default) dry_run default true; seeds GA4/Clarity/Meta; delegates POC access | workflow_dispatch |
| 504 | Google - GTM Container Backups (weekly export) 504-google-gtm-backup.yml | GOOGLE | Reads read-only exports; live-version JSON artifacts (90d) | schedule, workflow_dispatch |
| 505 | Google - GA4 Property Provision (per-charity) 505-google-ga-property-provision.yml | GOOGLE | Writes (dry-run default) dry_run default true; one property per charity; idempotent by stream defaultUri | workflow_dispatch |
| 506 | Google - Fleet Telemetry Reachability 506-google-fleet-telemetry-reachability.yml | GOOGLE | Reads read-only; enumerates GTM containers, GA4 properties/streams and Search Console sites, cross-checks them against each site's SERVED HTML, and ranks traffic. Reports "no data" distinctly from "no traffic" so an unmeasured site never sorts as unpopular. Weekly Mon 07:23 + dispatch | schedule, workflow_dispatch |
WPMUDEV
1 workflows| # | Workflow | APIs | Safety | Runs on |
|---|---|---|---|---|
| 601 | WPMUDEV - Export Sites/Domains (Read-only) 601-wpmudev-export-sites.yml | WPMUDEV | Reads waits on wpmudev-prod approval | workflow_dispatch |
GitHub (Website + Repo)
35 workflows| # | Workflow | APIs | Safety | Runs on |
|---|---|---|---|---|
| 701 | Website - Provision (Issue Assigned) 701-website-provision.yml | CFRepo | Writes (gated) `repo` chained behind `dns` approval | issues, repository_dispatch, workflow_dispatch |
| 702 | Domain - Deploy Static Clone to FFC-EX Repo 702-ffc-ex-clone-deploy.yml | Writes (gated) ungated preflight (repo must exist; refuses live sites and sibling-domain repos unless `force=true`); opens a draft PR (never pushes); serialized | workflow_dispatch | |
| 703 | Sites List - Generate (CSV + JSON) 703-sites-list-generate.yml | GH | Writes (data PR only) dispatches read-only exports 201/108/601 (601 waits on its own wpmudev-prod gate); regenerates `sites-list/` CSV+JSON; opens a data PR (never pushes to `main`); weekly Mon 08:00Z + dispatch; serialized concurrency group; loads `wr-all-cbm-ffc-copilot-mcp-github-pat` β **held on credential scope**, not on a taxonomy gap (#915) | schedule, workflow_dispatch |
| 704 | Website - Analytics Wire 704-website-analytics-wire.yml | GH | Writes (dry-run default) opens a draft PR (never pushes); `dry_run` (default true); idempotent | workflow_dispatch |
| 705 | Website - Capture WordPress Site (REST API + Scrape) 705-website-wordpress-capture.yml | WP | Reads read-only capture of a public site; `mode=inspect` default (probe only), `capture` writes only the run artifact; politeness delay default 250ms; serialized per domain; offline `--self-test` gates the logic before any live request | workflow_dispatch |
| 706 | Website - WordPress to Pages Conversion (end to end) 706-website-wordpress-to-pages.yml | WP | Writes (gated) `resolve` and `convert` are ungated and read-only; `mode=convert` default performs the whole conversion β capture, integrate, Next.js build, self-containment gate β and writes nothing, so it needs no approval; only `deliver` is gated and only `deliver` writes. `target_repo` is a required, separately-validated bare repo name (never derived from `domain`), so a capture of one site cannot be delivered to a repo nobody named. Forms are replaced with a `mailto:` block before the artifact is made, and `deliver` re-checks for a surviving `<form>` before committing β a static export has no form backend, so a surviving form would accept a visitor's message and drop it. `deliver` consumes the artifact the gate passed rather than re-capturing, so the charity's server is read once. Offline `--self-test` of every script the workflow runs gates every live request. | workflow_dispatch |
| 720 | Repo - Create GitHub Repo 720-create-repo.yml | Repo | Writes (gated) dispatch-only; creates an FFC-EX repo from a template (no dry_run β gate is the only stop); duplicate-repo warning text in inputs; visibility/Pages options set at creation | workflow_dispatch |
| 721 | Repo - Deploy GitHub Pages 721-deploy-pages.yml | Repo | plumbing | push, workflow_dispatch |
| 722 | Repo - CI Validate and Test 722-ci.yml | Repo | plumbing | merge_group, pull_request, push |
| 723 | Repo - CodeQL Security Analysis 723-codeql-analysis.yml | Repo | plumbing | merge_group, pull_request, push, schedule, workflow_dispatch |
| 724 | Repo - Initialize Labels 724-initialize-labels.yml | Repo | plumbing | workflow_dispatch |
| 725 | Repo - Sync Labels 725-sync-labels.yml | Repo | plumbing | push, workflow_dispatch |
| 726 | Repo - Rulesets + Settings Drift Audit 726-repo-rulesets-drift-audit.yml | Org | Reads report only; `read-all-cbm-ffc-copilot-mcp-github-pat` from KV over OIDC; ungated lane (#834) so the daily audit is never cancelled waiting at a gate | schedule, workflow_dispatch |
| 727 | Repo - Phantom Revert Guard 727-phantom-revert-guard.yml | Repo | plumbing | merge_group, pull_request, workflow_dispatch |
| 728 | Repo - AI Agent Hooks Validate 728-ai-agent-hooks-validate.yml | Repo | plumbing | merge_group, pull_request, push |
| 729 | Repo - Add Collaborator 729-repo-add-collaborator.yml | Repo | Writes (**live default**) β οΈ `dry_run` defaults to **false** | workflow_call, workflow_dispatch |
| 730 | Repo - Audit Environment Approval Gates 730-repo-audit-environment-gates.yml | Repo | Reads report only (environment reviewer config) | push, workflow_dispatch |
| 731 | Repo - Actions Run Metrics (30d per-workflow stats) 731-actions-run-metrics.yml | GH | Reads GITHUB_TOKEN read-only; JSON artifact | schedule, workflow_dispatch |
| 732 | Repo - Bulk Create GitHub Repos 732-bulk-create-repos.yml | Repo | Writes (gated) dispatch-only; creates FFC-EX-\<domain\> for every domain in a list under ONE approval instead of one-per-domain (720 stays the single-repo path); ungated preflight refuses per-domain on name collision or sibling-domain match before the gate is spent; no CNAME input and never touches DNS; `dry_run` default true | workflow_dispatch |
| 733 | Repo - Credential Rotation Reminders (quarterly) 733-credential-rotation-reminders.yml | GH | Writes (issues only) quarterly reminder issues; rotations stay human/gated | schedule, workflow_dispatch |
| 734 | Repo - Stale Waiting-Run Janitor 734-stale-waiting-run-janitor.yml | Repo | Writes (cancels runs) cancels runs left waiting >N days at a gate; never approves; dispatch dry-run supported | schedule, workflow_dispatch |
| 735 | Repo - Dependabot Affected Repos 735-repo-dependabot-affected-repos.yml | Org | Reads weekly org inventory (feeds smoke-protected waves); PR-only + auto-merge; reads on `read-all-cbm-ffc-copilot-mcp-github-pat`, PR on the 2-repo `wr-all-cbm-datasync-github-pat`, both from KV over OIDC on the ungated lane (#834, writer ruling #848) | schedule, workflow_dispatch |
| 736 | Repo - Archive / Application Denied (Admin) 736-repo-archive.yml | Repo | Writes (dry-run default) ungated preflight (live archive requires a matching successful dry-run within 48h; fails fast on missing/already-archived; warns on recent push/Pages/open-issue references); archive-only (reversible, never deletes); `dry_run` default true; typed `confirm_repo`; org-locked + denylist | workflow_dispatch |
| 737 | Repo - Claim Sync 737-claim-sync.yml | Repo | Writes (issues/labels only) syncs `claimed` label from linked PRs (pull_request event, GITHUB_TOKEN) + daily sweep reconciling this repo's claims against every open PR in the org β one `search/issues` read, writes only this repo's issues (ambient GITHUB_TOKEN β CBM_TOKEN is gated-env-only and empty on schedule); releases when no open PR in any repo still references the issue, or after 48h idle for a hand-labeled claim; no external API, ungated; sweep `dry_run` via dispatch | pull_request, schedule, workflow_dispatch |
| 738 | Repo - Fleet Smoke Engine Drift Audit 738-fleet-smoke-engine-drift-audit.yml | Org | Reads weekly SHA-256 byte-identity audit of `post-deploy-smoke.yml` across the fleet vs canonical (FFC-IN-FFC_Single_Page_Template@main); rolling issue upsert/close on divergence; GITHUB_TOKEN (public reads + own-repo issue), no external API, ungated | schedule, workflow_dispatch |
| 739 | Repo - Process Health Metrics Report 739-process-health-metrics.yml | GH | Reads weekly "monitor the monitors" REST sweep (backlog + data-pipeline signals) posted as a new comment on #719 with week-over-week trends (baseline read from the prior comment's hidden data block); the Conductor-silence verdict (#1215) additionally drives an assigned rolling issue, because #719 is read by the component the alarm is about (#1269); JSON artifact; GITHUB_TOKEN (own-repo issue/Actions reads + one comment + one rolling issue), no external API, ungated | schedule, workflow_dispatch |
| 740 | Repo - Scheduled Workflow Failure Alert (rolling issue) 740-scheduled-workflow-failure-alert.yml | GH | Writes (issues only) **polls** every scheduled hub workflow β and, since #1296, `Security Audit` in the four core satellite repos, whose scheduled workflows had no alerting at all β twice hourly (`schedule` + `workflow_dispatch`; the `workflow_run` event has never fired in this repo β #843) and upserts one rolling issue per watched workflow (marker keyed by workflow name, so only that workflow's own green run closes it); absorbs the retired 732 Google lane (502/504); reports `cancelled`/`timed_out` as well as `failure`, latest completed default-branch run only; does **not** alert on a declined/expired approval gate (no job `failure` + β₯1 job `cancelled` β logged via `core.notice`, not an alert) β an unreadable job list still alerts, and a watched name matching no workflow fails the run loudly, as does a repo whose workflow list cannot be read (a sweep that has gone blind is never reported as a clean one); the run summary prints `repos/watched/checked/failing/...` so an empty result is distinguishable from a sweep that read nothing, `MAX_NEW_ALERTS_PER_RUN` bounds a first sweep, and a `dry_run` dispatch input rehearses the sweep without writing an issue; ambient GITHUB_TOKEN, no external API, ungated by design (must not be blocked by the gate it watches) | schedule, workflow_dispatch |
| 741 | Repo - Fleet Security Audit Coverage 741-fleet-security-audit-coverage.yml | Org | Reads weekly coverage audit of dependency-vulnerability detection across the FFC-EX fleet (a repo counts as covered only with BOTH `security-audit.yml` and an `audit:high` script) PLUS a lockfile-resolution check β real `npm ci --dry-run` per Node repo, since a lockfile that exists but does not resolve makes the nightly audit scan nothing (#889, ledger L06); rolling issue upsert/close on any gap; reports only β never bumps a dependency or writes to a fleet repo; GITHUB_TOKEN (public reads + own-repo issue), npm registry read-only, ungated | schedule, workflow_dispatch |
| 742 | Repo - Fleet Security Audit Backfill 742-fleet-security-audit-backfill.yml | Org | Writes (dry-run default) closes the gap 741 measures: opens one PR per uncovered FFC-EX repo adding BOTH halves of the audit pair (`security-audit.yml` + the `audit:high` script) in a single commit, never a `partial`; cron staggered deterministically by repo name across 06:00β06:59, skipping the crowded `:17`; `dry_run` (default true) runs the ungated `plan` job only, so a preview needs no approval; never pushes a target's default branch and never bumps a dependency (#822 owns remediation); repos with no `package-lock.json` are reported, not wired | workflow_dispatch |
| 743 | Website - Fleet Security Header Audit 743-fleet-security-header-audit.yml | Reads weekly audit of the security headers each FFC-delivered site ACTUALLY SERVES (HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, CSP), measured by fetching the live response and never by looking for a file in a repo β `public/_headers` is inert on this stack, so a source-file check reports coverage that does not exist (#884); scope is every domain in an FFC Cloudflare zone (derived column, not the curated host category); report-only CSP does not count as a CSP and a `<meta>` tag satisfies nothing; rolling issue upsert/close on any gap; plain HTTPS GETs to public sites, no credentials, no external API, GITHUB_TOKEN for the issue only, ungated | schedule, workflow_dispatch | |
| 744 | Repo - Public Feed Freshness 744-repo-public-feed-freshness.yml | GH | Reads daily OUTCOME check on the published Agentic OS status feed in `FFC-IN-ffcadmin.org` (`agentic-os-status.json`, both the `public/data/` and `src/data/` copies), asserting how old the timestamp the public page actually serves is β the three existing mechanisms are all PROCESS checks (#921 watches whether 502 succeeds, #908 watches PRs stuck behind `main`, #771 watches drift between the two copies) and every one of them was green while the page sat three days stale (#977); stale after 36h, so one missed daily tick does not report; a path that is missing, unreadable, or carries an absent/empty/non-ISO/future `generated_at` is a finding and never a pass, and a registered path the run produced no observation for is reported rather than skipped; rolling issue upsert/close; **deliberately holds no Key Vault credential** β ambient GITHUB_TOKEN only, since a monitor depending on the credential whose death it reveals goes dark when it is needed; ungated | schedule, workflow_dispatch |
| 745 | Repo - Agentic OS Board Audit 745-agentic-os-board-audit.yml | GH | Reads daily audit of the public Agentic OS board (org project #9) against the real backlog, running `scripts/audit-agentic-os-board.py` β which #968 shipped and nothing invoked, making it assurance only when a human remembered (#969); the board has NO auto-add so every card and Status is placed by hand and the audit is the compensating control, which on a manual process has to be automatic or it inherits the same failure mode; reports the three sets (missing from board / no Status / closed-or-merged but not `Done`) to the Conductor Log and is SILENT when clean, following 734; a run that could not read one of its two sides is reported as INCOMPLETE and never as clean, since the script exits 1 for both findings and enumeration failure and the exit code alone cannot tell them apart; read-only against GitHub (adds no card, sets no Status, closes nothing), Key Vault PAT on the reader identity for the org-level ProjectsV2 read the ambient token cannot do, ambient GITHUB_TOKEN for the issue comment, ungated | schedule, workflow_dispatch |
| 746 | Repo - Bulk Enable GitHub Pages 746-bulk-enable-pages.yml | Repo | Writes (gated) dispatch-only follow-up to 732: enables Pages (build_type=workflow, GitHub-default URL only, no CNAME/DNS) and dispatches deploy.yml on EXISTING FFC-EX-\<domain\> repos under ONE approval; 732 can only set Pages at creation time and its own preflight skips a domain whose repo already exists, so this is the only automated path to turn Pages on afterward; ungated preflight skips a domain with no matching repo or no deploy.yml on the default branch, without failing the rest of the batch; idempotent Pages-enable (GET before POST, never touches an already-configured β possibly custom-domain β repo); `dry_run` default true | workflow_dispatch |
| 747 | Repo - Conductor Liveness 747-conductor-liveness.yml | GH | Reads two-hourly OUTCOME check on whether the Conductor β the locally-run routine that is the only actor with promotion and merge authority β is still working; it went quiet mid-run on 2026-09-14 for ~5 days and nothing noticed, because all 17 monitoring workflows (730-746) watch things that happen INSIDE Actions and the Conductor's only GitHub footprint is its comments on the Conductor Log (#719), so its absence is invisible to the fleet by construction (#1339); four signals reported BY NAME with measured values and never averaged β `conductor-silence` (age of the newest `Run N β START / END` comment, warn 6h / alert 12h against an observed ~3h cadence), `merge-silence` (age of the newest merged `agentic-os` PR, the independent second signal, warn 24h / alert 48h), `open-pr-cap` (count vs the worker cap of 3, CONTEXT ONLY β the pile stood at 4 all through the healthy window, so a level-triggered rule reports every ordinary day) and `pr-growth` (strict monotonic growth across 3 consecutive runs, which is the shape a stalled supervisor actually makes); an unreadable log, an empty comment page, a START/END pattern that matches nothing (ledger L215) or an absent/empty/non-ISO/future timestamp is a finding and never "alive"; history carried forward in the rolling issue body; rolling issue upsert/close; **deliberately holds no Key Vault credential and no environment** β ambient GITHUB_TOKEN only, since a liveness check must not be blocked by a gate only the absent supervisor can approve, nor die with a PAT it might need to report; ungated | schedule, workflow_dispatch |
| 748 | Repo - Template Provisioning Matrix 748-template-provisioning-matrix.yml | Repo | Reads proves 701's provisioning yields a site that passes its OWN CI on either template: for every template (Footer-Only, Single Page) x sample charity (`tests/fixtures/sample-charities.json`, all fictional) it clones the template's default branch, applies the charity through 701's real parse script and `scripts/Apply-WebsiteReactTemplate.ps1`, then runs that template's format/lint/unit/site-config/drift/build checks; the drift check is what catches Free For Charity's identity left under a charity's name; runs on PRs touching the provisioning path, weekly, and on demand; public template clones and a local build only, no credentials, no writes, ungated | pull_request, schedule, workflow_dispatch |
Candid (GuideStar)
2 workflows| # | Workflow | APIs | Safety | Runs on |
|---|---|---|---|---|
| 801 | Candid - Charity Check (EIN) 801-candid-charity-check.yml | CANDID | Reads read-only IRS-status lookup; public org data only | workflow_dispatch |
| 802 | Candid - Essentials Search 802-candid-essentials-search.yml | CANDID | Reads read-only profile search; public org data only | workflow_dispatch |
For AI agents & new admins
A machine-readable version of this catalog is published at https://ffcadmin.org/data/workflow-catalog.json (stable URL; source of truth: docs/workflow-catalog.json in the automation repository, regenerated on every workflow change β CI fails if it drifts). Schema: workflows[] with number, title, apis, file, triggers, environments, safetyLevel, approvalEnv, guard, category. To pick a workflow: match the first digit to the system you need to act on, prefer Reads before Writes, and always run write workflows with the default dry_run preview first.
Related: Technology Stack Β· Sites List