Skip to content

rules.yaml schema

The full grammar of the rules file. For prose-level explanation of each section, see Rule anatomy.

Top-level shape

rules:
  - tracker: ...           # required, picks the tracker
    tags: [foo, bar]       # optional, CLI tag selector
    # ... tracker-specific fields ...
    notify: { ... }        # optional, but typically present
    mutations: [ ... ]     # optional

# Workspace-level (used by every rule)
mailAliases: {}         # sendmail-style email-only forwarding/expansion
telegramUsers: {}       # email → Telegram chat ID
slackUsers: {}          # email → Slack user ID

Common rule fields

Every rule accepts:

Field Type Required Description
tracker string yes One of jira / linear / github / gitlab / shortcut
tags string | list no CLI tag selector. Accepts a scalar (tags: morning), a comma-separated scalar (tags: "morning, standup"), or a list (tags: [morning, standup]). A rule runs when the CLI invocation has no tag args (no filter), or when any of the rule's tags appears in the CLI args (OR-match). Untagged rules are skipped the moment any tag is requested.
active bool no Default true. Set false to disable a rule without deleting it
notify object typically yes See below
mutations array no See below

notify shape

notify:
  subject: "..."                       # required if notify is present
  followup: "..."               # optional, one-line intro in digest
  mailTo: "assignee, lead@example.com" # comma-separated; markers or literals
  cc: "..."                            # same shape as mailTo
  telegramChatId: "ID,ID"              # literal IDs only, no markers
  slackUserId: "Uxxxx,Uxxxx"           # literal IDs only, no markers
  columns: [Status, Priority, ...]     # meta chips per item

Recipient resolution: see Markers and Routing model.

columns values

Built-in: Project, Type, Status, Priority, Resolution, Assignee, Reporter, Components, Labels, Affects Versions, Fix Versions, Time Spent (hrs), Due Date, Created, Updated.

Magic: all-non-empty — expands to every populated standard field plus all discovered Jira custom field display names.

Custom fields (Jira only): any custom field's display name (e.g. Severity, "Story point estimate") auto-resolves via the field map populated at startup. See Custom fields below.

mutations shape

Two flavours depending on the tracker.

REST (jira, shortcut)

mutations:
  - verb: POST              # HTTP verb
    urlPattern: "..."       # URL template, marker-substituted
    body: |                 # request body, marker-substituted
      {"...": "..."}

GraphQL (linear, github, gitlab)

mutations:
  - mutation: |
      mutation { ... }      # raw GraphQL body, marker-substituted

The two are mutually exclusive: a linear / github / gitlab rule reads mutation: and discards any REST verb/urlPattern/body siblings, a jira / shortcut rule reads the REST shape and discards mutation:. Mixing them on one entry is silently truncated to the rule's flavour.

Per-tracker fields

jira

- tracker: jira
  filter: "project = INFRA AND assignee = currentUser()"
Field Type Notes
filter string Raw JQL — the same expression the Jira web search bar accepts

linear

Mutually-exclusive filter mode — exactly one of filter / filterRaw / viewId:

- tracker: linear
  filter: "issues assigned to me, not completed"
# or
- tracker: linear
  filterRaw:
    state:
      type:
        neq: completed
# or
- tracker: linear
  viewId: "0e8a3b41-1234-..."

github

- tracker: github
  filter: "is:open is:issue org:bigcorp label:urgent"

Single filter: field, raw GitHub search string. Multi-repo / org / is:issue / is:pr qualifiers all inside the string.

gitlab

- tracker: gitlab
  filter:
    state: opened
    labelName: [urgent]
    assigneeUsernames: [alice]

Structured chip mapping — keys match Query.issues GraphQL argument names verbatim. Include a narrowing chip (assigneeUsernames / authorUsername / milestoneTitle / iids) so the query stays bounded on busy instances. See GitLab tracker page for the full chip list and the breadth note.

shortcut

- tracker: shortcut
  filter: "state:\"In Progress\" type:bug !is:archived"

Single filter: field, raw Shortcut search-operator syntax.

Custom fields

Jira only. At startup Preesta calls GET /rest/api/?/field and builds a case-insensitive display name → customfield_NNNNN map. Reference a custom field in columns: by its display name:

notify:
  columns: [Status, Priority, "Story point estimate", Severity]

Render shapes (auto-detected per value):

Shape Render
scalar (string / number) as-is
JArray<string> comma-joined
JArray<JObject> with name/value/displayName keys extract that field per element, comma-joined (multi-select fields)
single-select JObject with value or name extract that field
anything else compact JSON

Empty / missing values render as nothing — no crash.

What if a rule is malformed?

Preesta logs an error for that rule and keeps processing the rest of the file. Common cases:

  • A Linear rule with zero or 2+ of filter / filterRaw / viewId — exactly one is required.
  • A GitHub or Shortcut rule with an empty or missing filter:.
  • A rule with no notify: and no mutations: — nothing for it to do.

If a digest you expected isn't going out, check the log first — there's usually one line naming the rule that got dropped and why.