---
title: "Repository selection"
description: "Which repositories a routine covers, the selector and glob syntax, and how schedules and webhooks fan out per repository."
section: "routines"
url: "https://forkline.dev/docs/routines/repositories"
markdown: "https://forkline.dev/docs/routines/repositories.md"
source: "src/content/docs/routines/repositories.mdx"
---

# Repository selection

> [!TIP]
> **Agent hint**
> Rules on this page that most often cause invalid routines:
>
> - `repositories` and `repositories.include` are **required**. `include` is the word `all` or a list of **1 to 50** selectors. A routine with only `exclude` is invalid, and so is `include: []`.
> - A selector has **exactly one** of `id` (a repository UUID) or `name`. A `name` must have the form `owner/name` (it contains a `/`); `*` and `?` stay inside one segment and only a whole segment `**` crosses `/`. Names are case-insensitive and at most 256 bytes.
> - `exclude` has the same selectors (up to 50) and always wins: a repository is covered when it is included **and not** excluded.
> - A **schedule** or **webhook** routine runs **once per selected repository** and must select **1 to 200** repositories at save time. An **event** routine is not fan-out limited: each event matches only the repository it came from.
> - Only repositories you have already connected to Forkline can be covered. New ones that match a glob are covered automatically.

`repositories` says which of your repositories a routine covers. The result is everything in `include` that is not in `exclude`:

```yaml
repositories:
  include:
    - name: "acme/api-*"
    - id: 7f1c2c3e-0000-4000-8000-000000000001
  exclude:
    - name: "acme/api-legacy"
```

| Field | Required | Values |
|-------|----------|--------|
| `repositories.include` | yes | `all`, or a list of 1 to 50 selectors |
| `repositories.exclude` | no | up to 50 selectors (an empty list excludes nothing). Omit it for none |

## Selectors

A selector is an object with exactly one of two keys:

| Selector | Matches | Notes |
|----------|---------|-------|
| `id: <uuid>` | The one repository with this Forkline repository ID | The ID is Forkline's own UUID (`forkline repo view <name>` prints the full `id`; `forkline repo list` shows a shortened ID, the full one with `-o wide` or `--json`), not the number the forge uses. Stable when the repository is renamed |
| `name: "<owner/name>"` | Every repository whose full name matches the glob | The full name is `owner/name`. On GitLab it is the nested path, `group/subgroup/project` |

Setting both keys, or neither, is the error `` set exactly one of `id` and `name` `` at the selector (`/repositories/include/0`). A `name` without a `/` is the error `` use the form `owner/name`, with `*` and `**` as wildcards `` at `/repositories/include/0/name`. An `id` that is not a UUID is an error at `.../id`.

### Name globs

| Pattern | Matches | Does not match |
|---------|---------|----------------|
| `acme/api` | `acme/api` (and `ACME/Api`: case does not matter) | `acme/api-core` |
| `acme/*` | every repository of `acme` | `acme/team/tool` (a `*` stays inside one segment) |
| `acme/api-*` | `acme/api-core`, `acme/api-web` | `acme/api` (the `-` is literal) |
| `acme/service-?` | `acme/service-a` | `acme/service-ab` |
| `*/docs` | the repository `docs` of any owner | `acme/team/docs` |
| `acme/**` | `acme/api` and nested `acme/team/api`, `acme/team/sub/api` (GitLab) | `other/api` |
| `**/infra` | `infra` under any path | `acme/infra-old` |

Rules: `*` matches any run of characters inside one `/`-separated segment (also none); `?` matches one character; a segment that is exactly `**` matches any number of segments, including none. `**` inside a segment (`acme/api**`) behaves like `*`. There are no character classes (`[a-z]`) and no braces (`{a,b}`): list two selectors instead. Matching is case-insensitive and applies to the whole name. A pattern is 1 to 256 bytes.

### `include: all`

`all` covers every repository you have connected, including ones you connect later. Use it for routines that a filter narrows anyway (a `comment.created` routine with a `command` filter), and avoid it for schedules and webhooks, where every covered repository starts its own run. `all` is **not** capped at 200: on a schedule or webhook, `all` is valid only while you have 1 to 200 connected repositories. Saving it with more is refused with `422` at `/repositories`, and a routine saved earlier whose account later grows above 200 is refused at every firing (outcome `fan_out_refused`, stage `fan_out`) and starts **no** run, not even for the first 200. Use `include` with globs and `exclude` to stay under 200.

### Excluding

`exclude` takes the same selectors and wins over `include`. Excluding a name that is not included does nothing.

```yaml routine
name: review-api-prs
description: Review pull requests of every acme API repository except the legacy one
on:
  events: [pr.opened, pr.updated]
repositories:
  include:
    - name: "acme/api-*"
    - id: 7f1c2c3e-0000-4000-8000-000000000001
  exclude:
    - name: "acme/api-legacy"
    - name: "acme/api-sandbox-*"
agent:
  prompt: |
    Review pull request #{{ pr.number }} in {{ repository.fullName }}.
```

```yaml routine
name: triage-all-issues
description: Triage new issues in every connected repository
on:
  events: [issue.opened]
repositories:
  include: all
  exclude:
    - name: "acme/playground"
agent:
  prompt: |
    Triage issue #{{ issue.number }} "{{ issue.title }}" in {{ repository.fullName }}.
```

A GitLab group with nested projects:

```yaml routine
name: review-platform-mrs
description: Review merge requests in every project below the platform group
on:
  events: [pr.opened]
repositories:
  include:
    - name: "platform/**"
agent:
  prompt: |
    Review merge request !{{ pr.number }} in {{ repository.fullName }}.
```

## Which repositories are considered

Only repositories that you added to Forkline and that belong to a connected, not deleted connector are considered (for GitHub, a repository whose GitHub App installation is connected). A selector that matches none of them, for example an `id` of a repository you removed or one that is not yours, simply matches nothing: it is not an error today. The selection is evaluated against your repositories **each time the routine fires**, not when you save it. A repository you connect later is covered at once if it matches the selection, and one you remove stops being covered.

## Event routines

An event routine (`on.events`) matches **only the repository the event came from**. When an event arrives, Forkline finds the repository (by connector and repository URL), then checks it against `include` and `exclude`. The routine starts at most one run for that event; [filters](https://forkline.dev/docs/routines/filters) then decide whether it starts. There is no fan-out and no limit of 200. A selection that matches no repository is accepted for an event routine and never runs until one matches.

## Fan-out

Schedules and custom webhooks fan out.

A schedule (`on.schedule`) or a custom webhook (`on.webhook`) is not tied to one repository, so it starts **one run per selected repository**:

- Each repository gets its own prompt: `repository.*` in the template is that repository, and the runner checks out that repository's default branch (see [Which ref a run checks out](https://forkline.dev/docs/routines/templates#which-ref-a-run-checks-out)).
- Each repository is checked, run and recorded on its own. The `when` condition is the only filter allowed here and sees `repository` (and `payload` on a webhook); it is evaluated per repository, so `when: '!repository.private'` skips the private ones. A repository Forkline has not synced yet reads as `repository.private == false` for schedules and webhooks. One repository failing, being filtered, being skipped because of [concurrency](https://forkline.dev/docs/routines/concurrency-and-limits) or lacking credits does not stop the others.
- A schedule that fires once and selects 6 repositories starts 6 runs, and costs 6 runs of credits. Check the count before you publish.
- A webhook call, or a Run now, returns the `task_ids` of the runs it started. A call that starts nothing returns a `reason`: `no_matching_repos` when the selection matches no repository, otherwise the most telling outcome (for example `credit_exhausted`).
- **Run now** starts one run per selected repository too. Through the API or CLI you can name a subset of the selection (`repositories`, `--repository`); a repository outside the selection is refused with `422` `repository_not_selected`. See [Run now](https://forkline.dev/docs/routines/testing-and-activity#run-now).
- Retries of the same occurrence do not start the run twice: a schedule occurrence is keyed by its scheduled time and repository, and a webhook call by its `Idempotency-Key` header and repository (without the header every call runs).

### Limits and what happens at them

| Situation | When | Result |
|-----------|------|--------|
| Selection matches **0** repositories (schedule, webhook) | Save and publish | Refused with status `422` at `/repositories`: `` the selection matches none of your repositories, so the routine would never run ``. `forkline routine validate` and `POST /v1/routines/validate` return the same `422`. Only an **event** routine whose selection matches nothing can be saved; `validate` reports it as the warning `no_repositories` ("No repository matches this selection right now. The routine won't run until one does.") |
| Selection matches more than **200** repositories (schedule, webhook) | Save and publish | Refused with status `422` at `/repositories`: `` the selection matches {count} repositories, over the limit of 200; narrow it with `include` and `exclude` `` |
| The selection grows above 200 **after** the routine was saved (you connected more repositories) | Each firing | The whole firing is refused: outcome `fan_out_refused` at stage `fan_out`, and **no** repository gets a run. The reason reads: `This schedule now matches {n} repositories, above the limit of 200. Narrow the repository selection.` (`webhook` for a webhook). The next firing evaluates the selection again, so narrowing it fixes the routine |
| More than **50** selectors in `include` or `exclude` | Save | `422` at `/repositories/include` or `/repositories/exclude`: `at most 50 selectors, this has {n}`. Use globs |
| Event routine | | Never limited by the fan-out cap |

The firing is refused as a whole, never partly: a routine that matches 214 repositories does not run on the first 200. Narrow the selection (more `exclude` entries, or a tighter glob) and save the routine.

New repositories that match a glob are picked up automatically. This is the way a routine can cross the limit unnoticed, so schedules and webhooks should use selective patterns (`acme/api-*`) rather than `all`, or an `exclude` list.

## Schedules and webhooks in practice

```yaml routine
name: weekly-dependency-update
description: Update dependencies in the Go services every Monday
on:
  schedule:
    cron: "0 6 * * 1"
    timezone: Europe/Berlin
repositories:
  include:
    - name: "acme/svc-*"
  exclude:
    - name: "acme/svc-archive"
filters:
  when: '!repository.private'
agent:
  prompt: |
    Update the dependencies of {{ repository.fullName }} (default branch {{ repository.defaultBranch }}).
    Open a pull request with the changes.
```

```yaml routine
name: deploy-failed
description: Investigate a failed deployment reported by the pipeline for the one repository it names
on:
  webhook: {}
repositories:
  include:
    - name: "acme/api"
agent:
  prompt: |
    A deployment of {{ repository.fullName }} failed: {{ payload.message | default("no message", true) }}
    Find out why and open an issue.
```

## Invalid selections

A `name` that is not `owner/name`:

```yaml routine-invalid pointer=/repositories/include/0/name code=selector_name_format
name: bad-name
description: Selects by a name without an owner
on:
  events: [pr.opened]
repositories:
  include:
    - name: "api-*"
agent:
  prompt: |
    Review pull request #{{ pr.number }}.
```

Fix: `name: "acme/api-*"`, or `name: "*/api-*"` for any owner.

A selector with both keys:

```yaml routine-invalid pointer=/repositories/include/0
name: both-keys
description: Sets id and name on one selector
on:
  events: [pr.opened]
repositories:
  include:
    - id: 7f1c2c3e-0000-4000-8000-000000000001
      name: "acme/api"
agent:
  prompt: |
    Review pull request #{{ pr.number }}.
```

Fix: use two selectors, or only one key.

An empty include list:

```yaml routine-invalid pointer=/repositories/include
name: nothing-included
description: Includes no repository
on:
  events: [pr.opened]
repositories:
  include: []
agent:
  prompt: |
    Review pull request #{{ pr.number }}.
```

Fix: `include: all`, or list at least one selector. A routine that should not run yet is a draft, not an empty selection.

An `id` that is not a UUID (it is the Forkline repository ID, not the forge's number):

```yaml routine-invalid pointer=/repositories/include/0/id
name: bad-id
description: Uses a forge repository number as the id
on:
  events: [pr.opened]
repositories:
  include:
    - id: "12345"
agent:
  prompt: |
    Review pull request #{{ pr.number }}.
```

A bad name in `exclude` is reported at `/repositories/exclude/{i}/name`:

```yaml routine-invalid pointer=/repositories/exclude/0/name code=selector_name_format
name: bad-exclude
description: Excludes a name without an owner
on:
  events: [pr.opened]
repositories:
  include: all
  exclude:
    - name: "legacy"
agent:
  prompt: |
    Review pull request #{{ pr.number }}.
```

Only `exclude` is not enough, `include` is required (the pointer is the missing key, `/repositories/include`):

```yaml routine-invalid pointer=/repositories/include
name: only-exclude
description: Excludes without including
on:
  events: [pr.opened]
repositories:
  exclude:
    - name: "acme/legacy"
agent:
  prompt: |
    Review pull request #{{ pr.number }}.
```

Selector errors that depend on your account (more than 200 repositories, no repository at all) cannot be reproduced from the file alone; they are listed in [Errors that need more than the file](https://forkline.dev/docs/routines/errors#errors-that-need-more-than-the-file).
