Workflow recipes
Copy-pasteable workflows for the Action, and the reasoning behind the parts that are easy to get wrong.
All of these live in examples/workflows/ too.
Action inputs
| Input | Default | Notes |
|---|---|---|
jev-key | — | Required. Pass a secret, never a literal. |
github-token | ${{ github.token }} | |
config-path | .github/dispatch.yml | |
templates-path | .github/dispatch-templates | |
mode | from config | shadow, suggest, or auto |
command | triage | triage, escalate, or backfill |
dry-run | false | Decide and log, whatever the mode says |
upload-log | true | Write .dispatch/decisions.jsonl |
Action outputs
| Output | Meaning |
|---|---|
decision-key | Idempotency key of the decision made |
applied | Number of actions applied |
suggested | Number proposed but not applied |
suppressed | Number a rule wanted but a gate blocked |
mode | The mode the run actually used |
Day 0: shadow mode with a decision log
Start here. This writes nothing to the repository at all.
yaml
name: Dispatch
on:
issues:
types: [opened, edited, reopened]
pull_request:
types: [opened, edited, ready_for_review, synchronize]
issue_comment:
types: [created]
permissions:
contents: read
issues: write
pull-requests: write
concurrency:
group: dispatch-${{ github.event.issue.number || github.event.pull_request.number }}
cancel-in-progress: false
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: JeelGajera/Dispatch/apps/action@v0.1.0
id: dispatch
with:
jev-key: ${{ secrets.TYPESAFE_API_KEY }}
- name: upload the decision log
if: always()
uses: actions/upload-artifact@v4
with:
name: dispatch-decisions-${{ github.run_id }}
path: .dispatch/decisions.jsonl
if-no-files-found: ignore
retention-days: 30The permissions block still grants writes even in shadow mode. That is deliberate: when you promote to suggest you change one line in .github/dispatch.yml rather than editing the workflow, and nothing about the workflow has to be re-reviewed.
Why concurrency matters
GitHub has undocumented secondary rate limits on content creation, and exceeding them blocks an installation rather than throttling it. Two runs racing on the same item can also produce two comments, which breaks the one-comment-per-item guarantee.
cancel-in-progress: false because a cancelled triage wastes the provider call it already paid for.
Nightly escalations
Some rules depend on time passing. "This has been waiting on a reproduction for fourteen days" is not an event GitHub sends, so it needs a schedule.
yaml
name: Dispatch escalations
on:
schedule:
- cron: '30 3 * * *'
workflow_dispatch:
permissions:
contents: read
issues: write
pull-requests: write
jobs:
escalate:
runs-on: ubuntu-latest
steps:
- uses: JeelGajera/Dispatch/apps/action@v0.1.0
with:
jev-key: ${{ secrets.TYPESAFE_API_KEY }}
command: escalateMatching configuration:
yaml
escalations:
staleNeedsRepro:
afterDays: 14
then:
- { op: comment, template: stale.md, key: stale }
- { op: close, reason: not_planned }
allowDestructive: [close] # required, or the close is suppressedPull requests from forks
Read this before using pull_request_target
pull_request from a fork gets a read-only token and no secrets. Correct and safe — and it means Dispatch cannot label the pull request or read your API key.
pull_request_target gets a write token and your secrets, and runs the workflow from the base branch. It is safe only if you never check out or execute the pull request's code. An attacker opening a pull request controls that code completely; running it with this token and these secrets is a repository takeover.
So the workflow below has no checkout step, and must never gain one.
yaml
name: Dispatch (fork pull requests)
on:
pull_request_target:
types: [opened, edited, ready_for_review, synchronize]
permissions:
contents: read
pull-requests: write
issues: write
concurrency:
group: dispatch-pr-${{ github.event.pull_request.number }}
cancel-in-progress: false
jobs:
triage:
runs-on: ubuntu-latest
steps:
# NO actions/checkout. See the warning above.
- uses: JeelGajera/Dispatch/apps/action@v0.1.0
with:
jev-key: ${{ secrets.TYPESAFE_API_KEY }}Dispatch reads pull request metadata through the API and executes nothing from the head. If you need to build or test fork code, do it in a separate pull_request workflow with no secrets.
Gate a merge on the triage result
Use the outputs to fail a check when something needs a human.
yaml
jobs:
triage:
runs-on: ubuntu-latest
outputs:
suppressed: ${{ steps.dispatch.outputs.suppressed }}
steps:
- uses: JeelGajera/Dispatch/apps/action@v0.1.0
id: dispatch
with:
jev-key: ${{ secrets.TYPESAFE_API_KEY }}
mode: auto
- name: needs a human
if: steps.dispatch.outputs.suggested != '0'
run: |
echo "Dispatch proposed ${{ steps.dispatch.outputs.suggested }} action(s) it was not confident enough to apply." >> "$GITHUB_STEP_SUMMARY"Resist making this exit 1 until dispatch eval shows calibration you trust. A blocking check driven by an uncalibrated threshold is how a useful tool becomes one people route around.
Try a config change without committing it
yaml
on:
workflow_dispatch:
inputs:
mode:
type: choice
options: [shadow, suggest, auto]
default: shadow
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: JeelGajera/Dispatch/apps/action@v0.1.0
with:
jev-key: ${{ secrets.TYPESAFE_API_KEY }}
mode: ${{ inputs.mode }}
dry-run: truedry-run: true decides and logs whatever the mode says, so you can see what autowould do on a live repository without it doing anything.
Pin the version
yaml
- uses: JeelGajera/Dispatch/apps/action@v0 # moving tag, follows every 0.x release
- uses: JeelGajera/Dispatch/apps/action@v0.1.0 # exact release
- uses: JeelGajera/Dispatch/apps/action@3f9a1c2 # pinned commit, most cautious@v0 is the usual choice: it picks up each 0.x release without a workflow edit. Pin an exact release or a commit when you want to decide yourself when the version moves, and let Dependabot open the bump as a pull request.
@latest is not available. GitHub resolves the part after @ as a git branch, tag or commit, and Dispatch publishes no tag by that name. The Latest badge on a Releases page is a release marker rather than a git tag, so it cannot be referenced from a workflow — a moving tag like @v0 is how that is done.
The Action runs from the committed dist/ bundle, so a pinned commit pins the exact code that will run. CI enforces that the committed bundle matches its sources.
Self-hosted runners
Nothing special is required, but note that the Action stores its decision log in the Actions cache, keyed per repository. On a self-hosted runner with a persistent workspace, .dispatch/ may survive between runs — harmless, since the cache is restored over it, but worth knowing if you are debugging why a decision was served from cache.
Use /dispatch recheck on the item, or --force from the CLI, to bypass it.