Configuration
A repo’s Fabric settings live in a file at its root: fabric.yaml. It is an
ordinary tracked file, versioned in the operation log like everything else. Edit
it in a change, propose, and accept, and the new settings take effect the moment
the change lands on trunk. There is no separate settings screen and no
out-of-band state.
Every setting is optional. A repo with no fabric.yaml behaves exactly as Fabric
did before the file existed, and an omitted key keeps its default. You only write
down what you want to change.
Where it lives and how it is read
fabric.yaml sits at the repository root. When Fabric needs a setting, it reads
the file from the trunk and parses it. Because the file is versioned, the settings
in force are always the ones on trunk, and you can see exactly when they changed by
looking at the file’s history.
A recommended header points an editor at this page and the machine-readable schema:
# Fabric repository configuration.
# Docs: https://fabricemr.com/docs/config/
# Schema: https://app.fabricemr.com/config/schema.json
The schema
fabric.yaml is validated against a published JSON
Schema. The schema is the validator: the same document your
editor can load for completion and inline checking is the one Fabric uses. Point
your editor’s YAML schema setting at https://app.fabricemr.com/config/schema.json
to get completion and error squiggles as you type.
Validation runs when you propose a change that edits fabric.yaml. A malformed
or out-of-spec file is rejected there, with a message naming the problem, so a bad
file is caught against the change that introduced it. A change that does not touch
fabric.yaml is never affected.
Referencing secrets
Some settings carry sensitive values: a notification webhook URL, a deploy token.
You do not want those bytes sitting in a shared, versioned file. Reference them by
name instead, with a ${secret.NAME} token:
notifications:
destinations:
ops_slack:
provider: slack
name: Ops Slack
webhook: ${secret.NOTIFY_OPS_SLACK_WEBHOOK}
The file records the name, never the value. The value lives encrypted in the repo’s
secret store, which you manage from the Secrets card on the organization
settings page. When Fabric needs the setting, it looks the name up in the store, decrypts the value, and uses it. The
decrypted value is held in memory for that one use. It is never written back into the
file, the operation log, the application logs, or any settings page. Those surfaces
show the name ${secret.NAME}, so anyone reading the file or the history sees which
secret is used, never its contents.
NAME is an uppercase env-var key: an uppercase letter followed by uppercase letters,
digits, or underscores (for example SLACK_WEBHOOK_URL), the same shape the secret
store enforces.
A reference must point at a secret the store already defines. If you propose a change
whose fabric.yaml names a secret that does not exist, propose is refused with a
message naming the missing secret, so the gap is caught against the change that
introduced it rather than at some later enforcement. Define the secret on the
organization settings page first, then propose.
Change approval
The approval section controls how many reviews a change needs before it can be
accepted into trunk.
approval:
required: 2 # distinct approving reviews needed to accept (default: 0)
allow_self_approve: false # may the change's author approve their own change? (default: false)
reviewers: # whose approvals count (default: anyone with write access)
- alice
- bob
requiredis the number of distinct approving reviews a change needs. The default is0, which accepts without any approval, matching a repo that has nofabric.yaml.allow_self_approvedecides whether the change author’s own approval counts toward the total. It defaults tofalse, so by default someone other than the author must approve.reviewersrestricts whose approvals count. Leave it empty (the default) and an approval from anyone with write access counts. List identities and only their approvals count.
Record an approval with fabric approve (or fabric approve --request-changes for
the opposite). When you run fabric accept, Fabric counts the distinct approving
reviews and lands the change only if the policy is met. If it is not, accept is
refused with a message saying how many approvals are still needed.
An approval is a judgment of specific content, so it is pinned to the change’s tip at the moment it was recorded. Pushing more ops moves the tip and clears the prior approvals: the change has to be approved again against its new content. This is why you cannot approve a change, quietly amend it, and land the amended version unreviewed.
A change is judged by the policy it forked from
A change is measured against the approval policy in force at the point it forked
from trunk, not the policy written inside the change itself. This matters when a
change raises the bar. If a change edits fabric.yaml to require more approvals,
that change is still judged by the old policy, so it can be accepted normally and
the stricter rule applies to changes that fork after it. Raising the requirement
never traps the change that raises it.
Required checks
The checks section blocks accept while a required check is still running or has
failed. Without it, a failing or in-flight check does not stop a change from landing;
with it, the check has to pass first.
checks:
required: # checks that must all pass before accept (default: none)
- test
- .github/workflows/ci.yml / build
requiredlists the checks that must have passed in the change’s latest run batch. Each name matches a check either by its bare job name (for exampletest) or by its fullworkflow / jobtitle (for example.github/workflows/ci.yml / build). The default is an empty list, which gates on nothing, matching a repo that has nofabric.yaml.
When you run fabric accept, Fabric reads the change’s most recent run and lands it
only when every required check has passed. A required check that is missing, still
running, or failed refuses the accept with a message naming the checks that are
blocking. Re-run checks with fabric checks (or the Action runs view) and accept
again once they are green. Like the approval policy, the required-checks gate is
judged by the policy the change forked from, so tightening it never traps the change
that tightens it.
Require a ticket
The tickets section ties every change to a ticket. With it on, a change must be
linked to exactly one existing ticket before it can be proposed or accepted, and a
second open change may not reference a ticket that already has a live one.
tickets:
required: true # every change must reference one ticket (default: false)
required(defaultfalse) turns the policy on. When true:- A change must be linked to an existing ticket to be proposed or accepted.
Link one with
fabric change create --ticket TIX-Norfabric change ticket TIX-N. - A second open (draft or proposed) change may not reference a ticket that already has a live one, so two changes never compete on the same ticket.
- Multiple accepted changes per ticket over time are always allowed. Fabric is append-only, so a follow-up fix or revert is a new change against the same ticket, which is honest history rather than fragmentation.
- A change must be linked to an existing ticket to be proposed or accepted.
Link one with
Like the other gates, the ticket policy is judged by the config the change forked from, so turning it on never traps a change that predates it.
Manual workflow runs
Workflows run automatically on the change lifecycle: a push-triggered workflow runs
when a change is accepted to trunk, a pull_request-triggered one when a change is
proposed. A workflow that declares on: workflow_dispatch can also be run by hand from
its file page in the web UI, so you can trigger it whenever you want instead of waiting
for propose or accept.
on:
workflow_dispatch:
inputs:
environment:
description: Where to deploy
type: choice
options: [staging, production]
default: staging
version:
type: string
required: true
dry_run:
type: boolean
default: true
When you open such a workflow at a change, Fabric shows a form built from its declared
inputs. Supported types are string, number, boolean (a checkbox), and choice (a
dropdown of options). Each input can set a default, and a required input has to be
filled in before the run starts. Submitting the form runs the workflow with those values,
available in steps as ${{ inputs.<name> }} and ${{ github.event.inputs.<name> }}. A
workflow that does not declare workflow_dispatch shows the plain Run button instead, with
no inputs form. A manual run fires only the one workflow you dispatched, the same as GitHub:
other dispatchable workflows in the repo are left untouched. Manual runs show up in the
Action runs view under the “manual” trigger.
Scheduled runs
A workflow that declares on: schedule runs automatically on a cron cadence, with no change
and no person involved. Fabric reads the cron expressions from your trunk workflow files and
fires a run when each one comes due.
on:
schedule:
- cron: "0 * * * *" # every hour, on the hour
- cron: "30 2 * * 1" # 02:30 every Monday
jobs:
nightly:
runs-on: ubuntu-latest
steps:
- run: ./scripts/report.sh
Cron uses the standard five-field syntax (minute, hour, day-of-month, month, day-of-week) in
UTC, the same format and timezone as GitHub Actions. A workflow may list several cron
entries; each fires on its own schedule. The finest resolution is one minute.
A scheduled run is planned against the repo’s root trunk: it materializes the current trunk
frontier and runs the workflow’s on: schedule jobs against it, exactly as an accepted change
would. A cron only reads the workflows on trunk, so a schedule you add takes effect once the
change that adds it is accepted, and a schedule fires the same way no matter which hub instance
happens to be running: it fires exactly once per due tick across the whole deployment, never
twice.
Because a scheduled run has no human proposing or reviewing it, Fabric treats it like an
untrusted trigger: it counts against your organization’s run quota and it runs with no
secrets injected at any scope (${{ secrets.* }} resolves empty; ${{ vars.* }} still
resolves, since variables are non-secret). Use scheduled runs for checks, reports,
and housekeeping that do not need credentials; keep secret-bearing deploys on the change
lifecycle or a manual dispatch. Scheduled runs show up in the Action runs view under the
“scheduled” trigger.
Custom runner labels
Every workflow job names the runner it wants with runs-on:. Out of the box Fabric ships
four labels, the same ones GitHub-hosted Linux runners use:
ubuntu-latestubuntu-24.04ubuntu-22.04ubuntu-20.04
To run a job on something else, declare a custom label in .fabric/runners.toml, a
versioned file in your repository:
[[runner]]
label = "gpu-large"
image = "catthehacker/ubuntu:act-22.04"
group = "gpu" # optional: organizes related labels into a named group
[[runner]]
label = "arm64"
image = "catthehacker/ubuntu:act-latest"
A job can then select it:
jobs:
train:
runs-on: gpu-large
steps:
- run: ./scripts/train.sh
Each [[runner]] maps one label to a runner image. Labels are case-insensitive. You
cannot redefine one of the four built-in labels, and a runner entry must set both a label
and an image, so a typo fails the run with a clear message instead of quietly creating a
label that never matches.
The image picks the container a job runs in when the hub executes checks in Docker mode.
On the Railway-native runner, which runs each job directly on the host process (no
container), the image is metadata and a custom label runs on the host just like a built-in
one; the label still has to be declared so the job resolves to a real runner.
A job whose runs-on: label is neither built-in nor declared in .fabric/runners.toml
fails with a clear error naming the unknown label and listing the labels that are
configured. Fabric never silently falls back to a default runner: an unrecognized label is
always a hard failure, so a mistyped or removed label surfaces immediately.
The optional group field organizes related labels under a named runner group, a label you
can use to describe a pool of similar runners. Grouping is descriptive today; routing a job
to a specific runner pool by group is not yet wired up.
Workflow secrets and variables
Workflows resolve two kinds of named values at run time: secrets (${{ secrets.NAME }}),
which are encrypted and masked from logs, and variables (${{ vars.NAME }}), which are
plaintext configuration and appear in logs as-is. Both are managed from the organization
settings page and both come in three scopes, matching how GitHub Actions works:
- Organization values apply to every workflow run in the workspace.
- Repository values apply to the repo’s runs (secrets live on the Secrets card; variables on the Variables card).
- Environment values apply to a run that targets a named environment.
When the same name is defined at more than one scope, the most specific wins: environment
overrides repository overrides organization. So a DEPLOY_TOKEN set at both the
organization and an environment resolves to the environment’s value for a run in that
environment, and to the organization’s value everywhere else.
Manage them under Automations & CI on the organization settings page:
- Secrets holds the repo’s secrets (the deploy token and anything a workflow reads as
${{ secrets.NAME }}). Organization & environment secrets holds the wider-scoped ones. Secret values are write-only: they are stored encrypted and never shown again. To change one, save a new value; to remove it, delete it. - Variables holds non-secret
${{ vars.NAME }}values at any of the three scopes. Variable values are plaintext, shown in the list, and not redacted from run logs, so put configuration here (a region, a feature flag, a build target), never a credential.
NAME is an uppercase env-var key (an uppercase letter followed by uppercase letters, digits,
or underscores, for example DEPLOY_TOKEN or NODE_ENV).
Secrets follow the same trust rule as the repo’s deploy token: a run only receives them when a
human the workspace trusts is behind it. An accepted change (a push run of the reviewed trunk
workflow) always gets them; a proposed change or a manual dispatch gets them only when its
author already holds write access; and an automated trigger with no human (a schedule, and the
API and dispatch triggers) gets none. Variables are non-secret, so they resolve for every
run regardless of trigger or trust, the same way GitHub exposes vars.* to fork pull requests.
API-triggered runs
You can start a run from outside the change lifecycle with an authenticated API call, the
Fabric analog of GitHub’s workflow_dispatch and repository_dispatch. This is how an
external system (a deploy tool, a webhook relay, another repo’s job) kicks off a run without a
person opening a change. Like a scheduled run it is planned against the repo’s root trunk
frontier and carries no change.
Authenticate with an API key (fabric token create, or mint one in org settings) and POST to
the dispatch endpoint. The repo is resolved from the key, so you never name it.
workflow_dispatch runs a single workflow that declares on: workflow_dispatch, with the
inputs you supply. Only the named workflow fires, not every dispatchable one:
curl -X POST "$HUB/v1/vcs/dispatch" \
-H "Authorization: Bearer $FABRIC_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event": "workflow_dispatch",
"workflow": "deploy.yml",
"inputs": { "environment": "staging" }
}'
The workflow’s declared inputs are the source of truth: defaults are applied and required inputs are enforced from the trunk copy of the file, so an unknown input is ignored and a missing required one is rejected.
repository_dispatch fires every workflow that declares on: repository_dispatch, passing
an event_type and an arbitrary client_payload. Inside the workflow they resolve as
github.event.action and github.event.client_payload.*:
curl -X POST "$HUB/v1/vcs/dispatch" \
-H "Authorization: Bearer $FABRIC_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event": "repository_dispatch",
"event_type": "deploy",
"client_payload": { "ref": "main", "force": true }
}'
The key needs write access; a read-only key is rejected (403). Because an API trigger has no
human proposing or reviewing it, Fabric treats it exactly like a scheduled run: it counts
against your organization’s run quota (over budget returns 429) and runs with no repo
secrets injected (${{ secrets.* }} resolves empty). API-triggered runs show up in the
Action runs view under the “dispatch” trigger.
Protected environments and deployment approvals
A workflow job can name a deployment target with the job-level environment key, exactly as on
GitHub Actions:
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # or: { name: production, url: https://prod.example.com }
steps:
- run: ./deploy.sh
When a run has a job that targets an environment, that environment’s scoped secrets and variables
resolve for the run (see the precedence above). Naming an environment is enough to pick up its
${{ secrets.* }} and ${{ vars.* }} values.
An environment becomes protected when you add one or more required reviewers to it. A run whose job targets a protected environment pauses before that job runs and waits for approval:
- The run shows a Pending approval status in the Action runs view, and the run detail shows a banner naming the environment and its required reviewers.
- Any one required reviewer can Approve (the run resumes and the job executes with the environment’s secrets) or Reject (the run fails, and the required-checks gate counts it as a failed check). Non-reviewers, including admins who are not on the reviewer list, can see the pending state but cannot decide.
Manage environments under Automations & CI on the organization settings page:
- Environments lists the repo’s environments. Add an environment by name (letters, digits,
dashes, underscores, and dots, for example
productionorstaging), then add required reviewers to protect it. An environment with no reviewers is unprotected: its scoped secrets and variables still resolve, but a run targeting it never pauses. - Removing every reviewer makes an environment unprotected again; deleting the environment removes its reviewers too.
Approving a deployment is a write action, so a required reviewer needs write access to act on a pending run. Reviewers are org members; add them from the environment’s reviewer list.
Concurrency
A workflow can declare a concurrency group so rapid re-pushes or re-proposes do not stack
up runs. Fabric honors the same concurrency key GitHub Actions uses, in either form:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true
The group is a key you choose. Any two runs that resolve to the same group are treated as
mutually exclusive. Expressions in the group expand against the run’s context, so
deploy-${{ github.ref }} scopes the group to one change, while a literal production
shares the group across every change in the repo. ${{ github.workflow }} resolves to the
workflow’s own name.
When a new run enters a group that already has one in flight:
- With
cancel-in-progress: true, the older run is canceled and the new one takes over. The canceled run shows as superseded in the Action runs view, distinct from a run you canceled by hand. - Without it (the default), the new run waits for the older one to finish, so same-group runs execute one at a time in order.
A workflow with no concurrency key is unbounded, exactly as before: its runs never cancel
or wait on each other.
Job timeouts
A job can declare timeout-minutes to bound how long it may run. Fabric honors the same key
GitHub Actions uses:
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- run: ./scripts/build.sh
A job that exceeds its timeout-minutes is terminated and recorded as a failed run, with the
reason (“job exceeded its timeout-minutes”) shown in its log. The bound is per job: one job
timing out does not stop the others in the run. You can also set timeout-minutes on an
individual step to bound that step alone.
Without timeout-minutes, a job runs under Fabric’s default whole-run cap of 10 minutes.
Declaring a longer timeout-minutes raises the cap for that run so a legitimately long job is
not cut off early. A ceiling of 6 hours applies to any single job regardless of the declared
value, so a run can never occupy a runner indefinitely.
If a long-running job is also a required check, raise the accept queue’s check_timeout to
match (see Queue settings). That is a separate wall-clock budget the
queue applies while a change waits to land, and it defaults to 10 minutes, so a check that runs
longer needs both its own timeout-minutes and a check_timeout large enough to let it finish.
Workflow annotations
Workflow steps can flag problems with the GitHub workflow commands ::error::,
::warning::, and ::notice::, each with optional file, line, and col params:
- run: echo "::error file=src/app.js,line=12,col=5::Missing semicolon"
Fabric parses these out of a job’s live log as it runs and shows them as a structured
summary at the top of the run detail, above the raw logs. Errors are listed first, then
warnings, then notices. When an annotation names a file, its location links into the
file view at that path (and line, when given), so you can jump straight from a failing
check to the code. The raw logs are untouched: annotations are an extra layer, not a
replacement.
Dependency caching
Workflows can cache dependencies between runs with the standard actions/cache action, so a
job restores an installed dependency directory instead of downloading and building it from
scratch every time. It works the same way it does on GitHub: give it a path to cache and a key,
and Fabric stores the directory after the run and restores it on the next run whose key matches.
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-
On a cache hit the path is restored before your install step runs; on a miss the step
installs normally and the directory is saved under key when the job finishes. restore-keys
are tried as prefixes when the exact key is not found, so a lockfile change still restores the
closest earlier cache and only re-fetches what changed.
Caches are scoped to your organization and repository: one repo never sees another’s caches, and one organization never sees another’s. A cache not read for a while is evicted automatically, so storage tracks what your workflows actually use. There is nothing to configure and no key to manage: caching is available to every run.
Status badge
Fabric serves an embeddable SVG badge that shows your repo’s latest trunk run status, the same green “passing” / red “failing” / amber “pending” you see in the Action runs view. Drop it in a README or a dashboard:

The <repo-id> is the id in the address bar of any repo page (for example the Files or Runs
view, /repos/<repo-id>/...). The badge reflects the most recent run that landed on trunk (an
accepted change), so it tracks what is actually shipped, not in-flight proposals or local
pre-flight runs. Before a repo has any trunk run it reads a neutral gray “no runs”.
The endpoint is public: it takes no login, because a README image is loaded without your session. It is scoped by the repo id itself and reveals nothing beyond that one repo’s pass/fail/pending, so it is safe to embed anywhere you would embed a GitHub Actions badge.
Two optional query parameters tune it:
?workflow=<name>scopes the badge to a single workflow file instead of the whole run. The name matches the workflow file with or without its extension, so?workflow=cimatches.fabric/workflows/ci.yaml.?label=<text>overrides the left-hand label (defaultchecks).

Roles
The roles section defines your organization’s custom roles and the permissions
each one grants. Roles are keyed by a short identifier; each role has a display
name, an optional description, and a list of permissions.
roles:
reviewer:
name: Reviewer
description: Reviews and approves changes
permissions:
- vcs:read
- vcs:write
- settings:read
editor:
name: Editor
permissions:
- vcs:read
- vcs:write
Each permission is a resource:action key from Fabric’s permission vocabulary,
such as vcs:write or settings:read. The schema lists the full set, and your
editor will complete and check them. The built-in Admin role grants every
permission and is defined in code, so you never list it here.
When the roles section is absent, Fabric falls back to the roles it stored before
roles moved into the file, so an organization that has not yet written any roles
keeps working unchanged. When the section is present, a role defined in the file
takes precedence over a stored role with the same key.
Editing roles from settings
You edit roles under Settings, Team, Roles. Creating, editing, or deleting a
role there does not write a database record directly: it produces a change that
edits fabric.yaml, the same as any other configuration edit. The change is
proposed and, if your approval policy allows, accepted immediately; otherwise it
waits for review. While a role change is awaiting review, the Roles page shows a
banner linking to the pending change, and the new permissions take effect only once
that change is accepted. Editing roles requires repository write access (vcs:write),
because it edits a versioned file.
Role definitions live in the file. Who holds each role (the assignment of a person to a role) is managed under Settings, Team and stays with your organization’s membership, because it points at a specific user account rather than at repository state.
Agent
The agent section holds org-wide agent settings. Today that is the voice
dictation hotkey.
agent:
voice_hotkey: ctrl+space
voice_hotkeyis the keyboard shortcut that starts voice dictation for everyone in the organization. When the key is absent, Fabric falls back to the hotkey stored for the organization, then to its built-in default.
A person’s own hotkey preference is not set here. It stays with their user account, because it is a personal choice rather than repository policy.
Session recording
The recording section controls whether the session recorder runs.
recording:
session_recording: true
session_recordingturns the recorder on or off for the whole organization. When the key is absent, the deploy-time default applies: off in production unless it was enabled at deploy, on everywhere else. Setting it here overrides that default without a redeploy.
Colors
The ui.colors section renames the badge and status color presets offered in
pickers. The palette itself is fixed, so every color keeps resolving to the same
built-in style; only the label shown for each preset changes.
ui:
colors:
- key: blue
label: In Review
- key: green
label: Accepted
- key: red
label: Blocked
keyis one of Fabric’s palette keys:gray,brown,orange,yellow,green,blue,purple,pink,red.labelis the name shown for that preset.
List only the presets you want to rename. When the section is absent, the built-in labels are used.
Automations
The automations section overrides the trigger of a workflow, keyed by the
workflow’s name.
automations:
Nightly Export:
trigger_type: scheduled
trigger_config:
cron: "0 2 * * *"
trigger_typeis the event class that starts the workflow, such asscheduledorchange_accepted.trigger_configis an optional filter compared against the trigger event. Omit it to match on the trigger type alone.
A workflow named in this section runs on the trigger the file gives it; a workflow not listed keeps its stored trigger. When the section is absent, every workflow keeps its stored trigger.
Editing settings from the app
Each of these sections has a card on the organization settings page (the
voice hotkey, session recording, colors, and automations). Saving a card does not
write a database record directly: it produces a change that edits fabric.yaml, the
same as any other configuration edit. The change is proposed and, if your approval
policy allows, accepted immediately; otherwise it waits for review, and the settings
page shows a banner linking to the pending change until it is accepted. Saving a
setting requires repository write access (vcs:write), because it edits a versioned
file.
Notifications
The notifications section routes version-control events to your team’s chat. It
has two parts: destinations, the chat sinks a message can go to, and
subscriptions, which event goes to which sink.
notifications:
destinations:
ops_slack:
provider: slack # slack | discord
name: Ops Slack # shown in settings
channel: "#ops" # optional (Slack)
webhook: ${secret.NOTIFY_OPS_SLACK_WEBHOOK}
enabled: true # optional, defaults to true
subscriptions:
- event_type: change.proposed # route this event...
sink: chat # ...to a chat destination...
destination: ops_slack # ...named by its key
- event_type: check.failed
sink: in_app
Each destination is keyed by a short identifier (a lowercase slug). A subscription names that key to send an event to it.
providerisslackordiscord.webhookis a${secret.NAME}reference to the delivery URL. It is never written into the file. Store the URL in the organization settings page’s Secrets card (or let the settings page store it for you) and reference it by name. See Referencing secrets.channelis an optional Slack channel; providers that do not use a channel ignore it.enableddefaults totrue; set it tofalseto keep a destination in the file without delivering to it.
A subscription’s event_type is one of change.proposed, change.accepted,
change.abandoned, or check.failed. Its sink is chat (with a
destination key) or in_app for an in-app notification, delivered to each
subscribed user’s Inbox in the web app.
When the notifications section is absent, Fabric falls back to the destinations
and subscriptions it stored before routing moved into the file, so an organization
that has not yet written any routing keeps delivering unchanged.
Editing notifications from settings
You edit routing under Settings, Notifications. Adding a destination, changing
its events, or removing it does not write a database record directly: it produces a
change that edits fabric.yaml, the same as any other configuration edit. When you
enter a webhook URL, Fabric stores it as an encrypted repo secret and writes only
the ${secret.NAME} reference into the file. The change is proposed and, if your
approval policy allows, accepted immediately; otherwise it waits for review, and the
page shows a banner linking to the pending change. Editing notifications requires
repository write access (vcs:write), because it edits a versioned file.
The Test button sends a sample message through a destination’s provider using the resolved webhook, so you can confirm delivery works. A destination is testable once its change is accepted and live.