new-loop-routine
The single source of truth for a repo's loop automation. Every repo gets one
loop-dispatch routine plus one committed caller workflow — identical except for
the repo. The routine is fired by the GitHub Action (via its Fire URL), so there are no
UI event triggers and none of the event-type-mixing limits they impose.
Why a GitHub Action, not UI triggers: the routines UI can't put different GitHub event
types (issue + PR + PR-review) on one routine, and each is a separate manual, un-scriptable
setup. A committed Actions workflow subscribes to all event types at once, routes at the
edge (route-event.sh), and fires the routine only when the event maps to a loop — so
non-matching events (a Dependabot dependencies label) cost nothing.
Standard routine config (identical for every repo)
| Field |
Value |
environment_id |
your ops cloud env — the one running the cloud-skill-sync setup script (from /schedule; account-specific, not written here). |
model |
claude-sonnet-5 — the dispatcher base; the loops pick their own subagent tier. |
allowed_tools |
["Bash","Read","Write","Edit","Glob","Grep","Skill","Task"]. |
sources |
the target repo, e.g. https://github.com/umbraco/<repo>. |
mcp_connections |
Slack + Claude_Code_Remote (for push) — connector UUIDs from /schedule (account-specific, not written here). |
| trigger |
none / disabled cron placeholder — the routine is fired by the Action's Fire URL, not a schedule or UI event. |
The routine's stored prompt is the Consolidated routine block in
references/routine-prompts.md.template (copy
verbatim, replace {{OWNER_REPO}}, no rewording). The Action appends the edge-resolved
route to each fire.
Never use fable. Never put secrets in the prompt or config.
Procedure
Preconditions (once per repo):
- The labels exist. Do not hand-write the list:
/ops-install creates every one of them,
on the repo each one's role implies, from plan-labels.sh. If you are standing up a routine on
a repo that has not been onboarded, run the installer first. (This step used to carry a
six-name list of its own, which had gone stale by six labels — ops/in-progress,
ops/auto-rework, ops/port, ops/proto-learning, ops/triaged and ops/loop-improvement
were all missing — and cited a self-learning-system.md that does not exist in this repo. A
copy of a list is a list that rots; the planner is the source.)
- Skills reach the env — paste
scripts/cloud-setup-stub.sh into the environment's Setup
script field. No variables and no token: the engine is public, so the clone is anonymous.
On an environment that already exists, bump the # rebuild: number as well, or it keeps
serving its cached snapshot. There is no list to add a skill to: cloud-skill-sync.sh delivers
every skill and agent in the repo, precisely so that adding one needs no second edit.
- Org Actions policy allows calling a reusable workflow from
umbraco/umbraco-ai-ops
(if the org restricts actions to "selected", allowlist it). Nothing else is needed: the engine
is public, so Actions resolves the reusable workflow without an access grant.
Stand it up:
- Create the routine (via
RemoteTrigger create) with the Standard config,
enabled: false, a cron placeholder, and the consolidated prompt. One per repo.
- Generate its token + Fire URL in the routines UI (Call via API → Generate
token). These are per-routine.
- Set two secrets on the repo (or the org, to share):
LOOP_DISPATCH_FIRE_URL (the
Fire URL) and LOOP_DISPATCH_TOKEN (the token) — gh secret set ….
- Commit the caller workflow — copy
references/loop-dispatch.yml.template
verbatim to the repo as .github/workflows/loop-dispatch.yml (open a PR).
- Smoke-test — label a throwaway issue
ops/ready-for-ai (Action fires → routine builds
a PR), and label a PR dependencies (Action computes loop=none → routine never fires).
Cross-repo consumers (issues and PRs in different repos)
When a product's issues live in a separate repo from its source (e.g.
Umbraco.Forms.Issues → the Forms code repo), the caller workflow is split across the two
repos — the same template, wired to different events in each:
- In the issues repo: subscribe to
issues (labelled ops/ready-for-ai / ops/auto-release)
and commit a .claude/ops-repo-meta.json there declaring topology.code. The router reads
it and emits target=<code repo>, so the routine works there while reading the issue here.
with.target_repo also does this and is deprecated — it was the same fact hand-written a
second time, in another file in another repo, with nothing to catch the two disagreeing.
- In the code repo: subscribe to
pull_request_target (labelled ops/auto-merge /
ops/auto-rework / ops/port) — those events fire where the PRs live, and the work is
already there, so nothing needs declaring.
A same-repo consumer (issues and PRs together) keeps a single workflow subscribing to both,
with no target_repo.
Rules
- One routine + one caller workflow per repo. The routine is fired by the Action's
Fire URL — no UI event triggers.
- Both templates are locked — the routine prompt (
routine-prompts.md.template) and the
caller workflow (loop-dispatch.yml.template) are copied verbatim; changing them means
editing those files in a PR, never hand-editing a live routine or repo workflow.
- Thin prompt. The routine prompt only invokes the skill; loop policy lives in the
loop skills and must not drift per repo.
- Standard config always. Don't hand-tune per repo beyond
sources/name and the two
secrets.
- Never use
fable.
1---2name: new-loop-routine3description: Stand up the standardised loop automation for a repo — one loop-dispatch routine per repo, fired by a committed GitHub Action (not UI event triggers), with identical env, model, tools, connections, and a thin prompt every time. The Action routes at the edge and only fires the routine on a real match. Use when onboarding a new repo to the loops, or to standardise/rewrite existing routines. Interactive/local.4---56# new-loop-routine78The **single source of truth** for a repo's loop automation. Every repo gets **one9`loop-dispatch` routine** plus **one committed caller workflow** — identical except for10the repo. The routine is fired by the GitHub Action (via its Fire URL), so there are **no11UI event triggers** and none of the event-type-mixing limits they impose.1213Why a GitHub Action, not UI triggers: the routines UI can't put different GitHub event14types (issue + PR + PR-review) on one routine, and each is a separate manual, un-scriptable15setup. A committed Actions workflow subscribes to all event types at once, routes at the16edge (`route-event.sh`), and fires the routine **only when the event maps to a loop** — so17non-matching events (a Dependabot `dependencies` label) cost nothing.1819## Standard routine config (identical for every repo)2021| Field | Value |22|---|---|23| `environment_id` | your ops cloud env — the one running the `cloud-skill-sync` setup script (from `/schedule`; account-specific, not written here). |24| `model` | `claude-sonnet-5` — the dispatcher base; the loops pick their own subagent tier. |25| `allowed_tools` | `["Bash","Read","Write","Edit","Glob","Grep","Skill","Task"]`. |26| `sources` | the target repo, e.g. `https://github.com/umbraco/<repo>`. |27| `mcp_connections` | Slack + Claude_Code_Remote (for push) — connector UUIDs from `/schedule` (account-specific, not written here). |28| trigger | **none / disabled cron placeholder** — the routine is fired by the Action's Fire URL, not a schedule or UI event. |2930The routine's stored prompt is the **Consolidated routine** block in31[`references/routine-prompts.md.template`](references/routine-prompts.md.template) (copy32verbatim, replace `{{OWNER_REPO}}`, no rewording). The Action appends the edge-resolved33route to each fire.3435Never use `fable`. Never put secrets in the prompt or config.3637## Procedure3839**Preconditions (once per repo):**401. **The labels exist.** Do not hand-write the list: **`/ops-install` creates every one of them**,41 on the repo each one's role implies, from `plan-labels.sh`. If you are standing up a routine on42 a repo that has not been onboarded, run the installer first. (This step used to carry a43 six-name list of its own, which had gone stale by six labels — `ops/in-progress`,44 `ops/auto-rework`, `ops/port`, `ops/proto-learning`, `ops/triaged` and `ops/loop-improvement`45 were all missing — and cited a `self-learning-system.md` that does not exist in this repo. A46 copy of a list is a list that rots; the planner is the source.)472. **Skills reach the env** — paste `scripts/cloud-setup-stub.sh` into the environment's **Setup48 script** field. No variables and no token: the engine is public, so the clone is anonymous.49 On an environment that already exists, **bump the `# rebuild:` number** as well, or it keeps50 serving its cached snapshot. There is no list to add a skill to: `cloud-skill-sync.sh` delivers51 **every** skill and agent in the repo, precisely so that adding one needs no second edit.523. **Org Actions policy** allows calling a reusable workflow from `umbraco/umbraco-ai-ops`53 (if the org restricts actions to "selected", allowlist it). Nothing else is needed: the engine54 is public, so Actions resolves the reusable workflow without an access grant.5556**Stand it up:**571. **Create the routine** (via `RemoteTrigger` `create`) with the [Standard config](#standard-routine-config-identical-for-every-repo),58 `enabled: false`, a cron placeholder, and the consolidated prompt. One per repo.592. **Generate its token + Fire URL** in the routines UI (*Call via API* → *Generate60 token*). These are per-routine.613. **Set two secrets** on the repo (or the org, to share): `LOOP_DISPATCH_FIRE_URL` (the62 Fire URL) and `LOOP_DISPATCH_TOKEN` (the token) — `gh secret set …`.634. **Commit the caller workflow** — copy [`references/loop-dispatch.yml.template`](references/loop-dispatch.yml.template)64 **verbatim** to the repo as `.github/workflows/loop-dispatch.yml` (open a PR).655. **Smoke-test** — label a throwaway issue `ops/ready-for-ai` (Action fires → routine builds66 a PR), and label a PR `dependencies` (Action computes `loop=none` → routine never fires).6768### Cross-repo consumers (issues and PRs in different repos)6970When a product's issues live in a **separate repo** from its source (e.g.71`Umbraco.Forms.Issues` → the Forms code repo), the caller workflow is **split across the two72repos** — the same template, wired to different events in each:7374- In the **issues repo**: subscribe to `issues` (labelled `ops/ready-for-ai` / `ops/auto-release`)75 and commit a `.claude/ops-repo-meta.json` there declaring **`topology.code`**. The router reads76 it and emits `target=<code repo>`, so the routine works there while reading the issue here.77 **`with.target_repo` also does this and is deprecated** — it was the same fact hand-written a78 second time, in another file in another repo, with nothing to catch the two disagreeing.79- In the **code repo**: subscribe to `pull_request_target` (labelled `ops/auto-merge` /80 `ops/auto-rework` / `ops/port`) — those events fire where the PRs live, and the work is81 already there, so nothing needs declaring.8283A same-repo consumer (issues and PRs together) keeps a single workflow subscribing to both,84with no `target_repo`.8586## Rules8788- **One routine + one caller workflow per repo.** The routine is fired by the Action's89 Fire URL — no UI event triggers.90- **Both templates are locked** — the routine prompt (`routine-prompts.md.template`) and the91 caller workflow (`loop-dispatch.yml.template`) are copied **verbatim**; changing them means92 editing those files **in a PR**, never hand-editing a live routine or repo workflow.93- **Thin prompt.** The routine prompt only invokes the skill; loop policy lives in the94 loop skills and must not drift per repo.95- **Standard config always.** Don't hand-tune per repo beyond `sources`/name and the two96 secrets.97- **Never use `fable`.**