Triggers

A trigger is the answer to one question: what makes this workflow run? rupu workflows can fire by hand, on a schedule, or in reaction to an SCM or issue-tracker event.

manual rupu workflow run cron clock rupu cron tick event poll / webhook rupu workflow run
Manual, cron, and event sources all converge on rupu, which dispatches one workflow run.

How workflows fire

Every workflow declares a trigger with a top-level trigger.on field in its .rupu/workflows/<name>.yaml. There are three modes; manual is the default when trigger is omitted.

trigger.onWhen it firesWhere it lives
manualrupu workflow run <name>every install
cronsystem cron / launchd invokes rupu cron tickevery install
eventmatching events appear via rupu cron tick (polled) or rupu webhook serve (live)polled + webhook tiers

The full schema, with cross-field validation rules:

trigger:
  on: manual | cron | event       # default: manual
  cron: "0 4 * * *"                # required when on: cron (5-field UTC)
  event: github.issue.opened       # required when on: event
  filter: "{{ event.repo.full_name == 'foo/bar' }}"  # optional, only when on: event
Validated at parse. cron: is only allowed when on: cron; event: and filter: only when on: event. A missing on: defaults to manual.

Cron

Cron-scheduled workflows run on a clock. rupu has no scheduling daemon of its own — you install a single system-cron or launchd entry that calls rupu cron tick once a minute, and each tick walks every cron-triggered workflow and fires those whose schedule matched between the persisted last_fired timestamp and now. It is idempotent at 1-minute granularity, so an overrunning tick never double-fires.

Install one entry in your crontab / launchd plist:

* * * * *  /usr/local/bin/rupu cron tick

Then give the workflow a schedule (5-field UTC expression):

# .rupu/workflows/nightly-audit.yaml
name: nightly-audit
trigger:
  on: cron
  cron: "0 3 * * *"   # every day at 03:00 UTC

steps:
  - id: scan
    agent: security-reviewer
    actions: []
    prompt: |
      Audit the repo for newly-added secrets in the last 24h.

Use rupu cron tick --dry-run to verify a crontab line without firing anything. rupu cron list is read-only — it prints every cron-triggered workflow plus its next firing time, handy before you add the cron entry.

Events

Event-triggered workflows react to activity in your SCM and issue trackers. rupu offers two delivery tiers that share the same workflow YAML and the same event vocabulary:

Polled sources

Configure the sources you want polled in ~/.rupu/config.toml (global) or <project>/.rupu/config.toml (project shadows global). The list is empty by default — rupu polls nothing until you ask it to.

[triggers]
poll_sources = [
  "github:Section9Labs/rupu",
  { source = "gitlab:my-org/my-repo", poll_interval = "15m" },
]

# Optional: cap events processed per source per tick. Default 50.
max_events_per_tick = 50

Each entry is either a bare "vendor:owner/repo" string (eligible every event tick) or an inline table with a source-local poll_interval cadence override (30s, 5m, 2h, 1d). The source model spans repos and trackers: github:owner/repo, gitlab:group/project, linear:<team-id>, and jira:<site>/<project> (or jira:<project> when [scm.jira].base_url is set).

Webhook serve

For sub-second latency or events the polled tier doesn't deliver, run rupu webhook serve under your own supervisor. Secrets come from environment variables only — never config files, never the keychain.

RUPU_GITHUB_WEBHOOK_SECRET=<your-webhook-secret> \
  rupu webhook serve --addr 0.0.0.0:8080

The receiver validates the vendor signature (X-Hub-Signature-256 for GitHub, X-Gitlab-Token for GitLab, Linear-Signature + timestamp freshness for Linear, X-Hub-Signature for Jira Cloud), maps the raw delivery onto the rupu event id, and fires matching workflows with {{event.*}} populated. Bind to 127.0.0.1 and front it with a TLS-terminating reverse proxy — rupu does not terminate TLS itself.

The event vocabulary

Each connector lifts events from the vendor's events API and maps them onto canonical rupu ids. You match against these in trigger.event:, and glob wildcards (*) work — github.issue.* matches every GitHub issue event, "*.pr.merged" matches cross-vendor, and "*" wakes on anything. A few common ids:

Event idMeaning
github.issue.openeda GitHub issue was opened
github.issue.labeleda label was added to a GitHub issue
github.pr.openeda GitHub pull request was opened
github.pr.review_requestedreview requested on a GitHub PR
github.pushcommits pushed to a GitHub repo
gitlab.mr.mergeda GitLab merge request was merged
linear.issue.updateda Linear issue changed (state/priority/…)
issue.queue_enteredsemantic alias: issue moved into a queue-like state

Beyond canonical vendor ids, rupu derives a broader semantic alias vocabulary (e.g. issue.queue_entered, pr.review_activity) from the same deliveries. Aliases are matchable alongside canonical ids, but one delivery still fires a workflow at most once. A complete event matrix and the templating metadata exposed as {{event.*}} live in docs/triggers.md.

A worked event workflow:

# .rupu/workflows/triage-incoming-issues.yaml
name: triage-incoming-issues
trigger:
  on: event
  event: github.issue.opened
  filter: "{{ event.repo.full_name == 'Section9Labs/rupu' }}"

steps:
  - id: comment_back
    agent: issue-commenter
    actions: []
    prompt: |
      Post a triage summary on issue
      {{ event.repo.full_name }}#{{ event.payload.issue.number }}.

Inspecting & operating

The cron subcommand is your window into both scheduled and polled-event triggers — there is no separate events daemon to manage.

Splitting the two flags lets each tier run at its own cadence:

* * * * *      rupu cron tick --skip-events     # cron only, every minute
*/5 * * * *    rupu cron tick --only-events     # events every 5 minutes

Per-source cadence is controlled by the poll_interval override on a poll_sources entry: it decides whether a source is due to be polled on a given rupu cron tick --only-events, without changing workflow-matching semantics. On a source's first poll rupu emits zero events and sets the cursor to "now," so adding a source never triggers a stampede over its history.

From trigger to autonomy

Triggers are the wake-up mechanism for autonomy. An autoflow is a workflow that runs unattended; its wake_on: matches the very same event vocabulary described here, so triggers are precisely how autoflows come to life. The events themselves originate from the SCM and issue-tracker connectors covered under Integrations — wire up a connector there, list it under poll_sources or point a webhook at it, and your workflows start reacting to the world.