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)¶
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¶
| 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¶
Single filter: field, raw GitHub search string. Multi-repo / org / is:issue / is:pr qualifiers all inside the string.
gitlab¶
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¶
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:
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 nomutations:— 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.