Automation — triggers, schedules, rota
Three tabs turn the console from an interactive tool into an operations station:
- Triggers react to events on the machine — a new email, a meeting reminder, a system toast — and run runnables or a headless agent turn. (Windows)
- Schedules run runnables, runbooks, or agent prompts on a clock.
- Rota is the shared working-hours gate over both, so automation only runs while someone is around to approve confirmations.
Everything automated goes through the same autonomy gate as chat — confirm-by-default, with run cards parked at Awaiting human until you approve or deny.
The chat agent can set all of this up in conversation — create_schedule,
manage_triggers, and manage_rota each show a confirmation card before
writing anything, and emit_event fires synthetic events for testing. The
reverse is deliberately impossible: a turn that was started by a trigger or
schedule can never use the config tools, so automation cannot rewrite its own
rules. See Configure the console.
Triggers (Windows)
A trigger binds an event source + event type (optionally narrowed by
regex match rules) to an action — either a deterministic runnable call
(mode = "rule") or a headless agent turn (mode = "agent") that follows
the trigger's rules with the full runnable toolset. The Triggers tab
shows your triggers and a run inbox where each matched event flows
through its lifecycle: Triggered → Triage → Awaiting human → Running →
Done / Failed.
Triggers only fire while the console is running, and never replay a backlog — your inbox and existing toasts are snapshotted at startup, so reopening the console doesn't re-handle old messages. An LLM provider must be configured for
mode = "agent".
Event sources
Source (source =) |
Events (event =) |
Notes |
|---|---|---|
outlook |
new_mail, reminder |
Uses the signed-in desktop Outlook client over COM — no extra dependency, rides your existing SSO session. |
notifications |
toast |
Every system toast (Teams, Slack, anything) via one listener. Needs the notifications extra and a one-time Windows consent grant. |
pip install "runspec-console[notifications]"
The Triggers tab shows each source's availability and the reason any is off.
Configuring triggers
Create and edit triggers on the Triggers tab, or describe one to the
agent ("when an email about an outage arrives from bigcustomer.com, …") and
approve the validated config it proposes. The tab's editor has a host/group +
runnable picker that lists the runnable's declared arguments as labelled
rows, so you rarely hand-write the config — but it lands in
runspec_triggers.toml as named tables you can also edit directly:
[triggers.vip-outage-triage]
source = "outlook"
event = "new_mail"
mode = "agent" # "agent" (LLM-in-the-loop) | "rule" (direct runnable)
autonomy = "confirm"
# agent mode: the rules the model follows for this trigger. (A scalar key, so
# it must appear BEFORE the [triggers.<name>.*] sub-tables below.)
rules = """
If this reports an outage, identify the affected host, run the diagnostics
runnable against it, and draft a short reply summarising findings. Never
restart a service or send mail without confirmation.
"""
# Cheap regex pre-filter on the event payload — all patterns must match.
[triggers.vip-outage-triage.match]
from = ".*@bigcustomer\\.com"
subject = "(?i)outage|down|urgent"
# Optional: let the agent save a suggested reply to Outlook Drafts (never sends).
[triggers.vip-outage-triage.reply]
draft = true
style = "Concise, professional. Acknowledge and state the next step."
Each trigger row has a "Trigger now" button that fires it with a
synthetic event — a pasted message plus key=value fields — so you can
test without waiting for a real event.
Key ordering inside a trigger table
Scalar keys (mode, rules, run, …) must come before that
trigger's sub-table headers — a key written after
[triggers.<name>.match] is parsed into the match table, emptying the
real key and adding a phantom match field no event can satisfy. The
Triggers tab surfaces a clear error if it spots this.
Rule mode — deterministic, no LLM
[triggers.log-incoming-ticket]
source = "outlook"
event = "new_mail"
mode = "rule"
run = "local__record-ticket" # host__runnable ("local" = this machine)
[triggers.log-incoming-ticket.map]
# left = the runnable's arg name; right = a template from the event payload
summary = "{{ subject }}"
sender = "{{ from }}"
queue = "support" # a literal (no template) is passed verbatim
map keys must be the runnable's real argument names — a wrong key fails
validation (visible in the Event log). Payload fields you can match on and
map from:
outlooknew_mail:from,to,subject,body,received,categories,entry_idoutlookreminder:subject,start,location,is_meeting,organizernotificationstoast:app,title,body,text
A rule trigger can also run a bounded list of runnables (an actions
array with action_mode = "sequential" or "parallel", up to 10) behind one
grouped confirmation, with an optional single LLM extraction step whose
output feeds the arg maps as {{ _llm.* }} templates. The Triggers editor
builds all of this.
Agent-mode triggers and action tools
An agent-mode trigger's turn has the full runnable toolset plus the agent's
action tools (create_document, run_runbook, create_schedule,
emit_event) — scoped per trigger by a runnables allowlist and a parallel
action_allow allowlist. With [triggers.<name>.reply] draft = true on a
new_mail trigger, the model can save a suggested reply to your Outlook
Drafts folder — the console never sends mail from a trigger; each draft
save is confirm-gated.
Debugging: the Event log
Below the run inbox, the Event log records every event the engine saw:
fired (which triggers ran), skipped (working hours) (matched but
rota-gated), or no match. First place to look when a trigger doesn't
fire: no match → your match regex; skipped → the rota; not in the log
at all → the source never delivered it (e.g. the mail landed outside the
Inbox). Every run card also keeps a timestamped timeline of stage changes
and each tool the agent ran.
Schedules
The Schedules tab runs work on a clock — cron or interval — with the same run inbox, event log, and lifecycle as triggers. A schedule's action can be:
- a runnable — optionally into one of its subcommands — targeting one host, a whole group (fans out), or a candidate host list (runs on the first connected one — failover);
- a runbook by id, with its arguments filled;
- a saved run by id — a deterministic preset whose own stored targeting decides the hosts, with the schedule's values filling its open params;
- a prompt — a headless agent turn on a fixed instruction ("check the backup log and flag anything unusual").
The agent creates schedules directly:
Schedule a check of the backup log on nas-01 every morning at 07:30 — if anything failed overnight, summarise it.
create_schedule shows a confirmation card; the schedule then lives on the
Schedules tab where you can pause, edit, or delete it.
Rota — working hours for automation
Because automated runs still need a human for confirmations, the Rota tab
restricts automation to a weekly working-hours grid: outside the window,
matching events and scheduled fires are skipped (dropped, not queued — no
backlog dump when the window opens). Off by default; per-day on/off with
start/end times (local machine time). A single trigger or schedule can opt
out with ignore_rota = true (e.g. an off-hours maintenance watcher).
[rota]
enabled = true
[rota.mon]
on = true
start = "09:00"
end = "17:30"
Drive it from your Outlook calendar instead of the grid with
source = "outlook": you're on shift whenever a calendar event whose subject
contains shift_marker (default "shift") covers the moment;
hold_during_meetings = true also pauses during meetings/OOO. If Outlook
can't be read, it falls back to the grid.
Two presence controls layer on top (both Windows):
- Take a break — a Rota-tab button suspends triggers and schedules for N minutes (15/30/60/custom) and auto-resumes, staying paused if the screen is still locked at the return time.
- Lock auto-suspend (default on) — reactive triggers don't fire while the screen is locked, with a live indicator on the Rota tab. It fails closed (an uncertain probe means paused), applies to all triggers with no per-trigger bypass, and exempts schedules (pre-authorised work, not reactions-while-away). It's a governance control, not a security boundary; every suppression is logged.
The chat agent is told the rota state and your configured triggers and schedules each turn — ask it "why didn't my trigger fire?" and it can reason about the gates. Interactive chat requests are never rota-blocked.
Microsoft 365 setup (Windows)
Two independent integrations:
Outlook desktop (COM) — the trigger source and the agent's
outlook_* tools use the signed-in desktop Outlook client directly. No
setup beyond having Outlook running and signed in; works where Graph is
blocked.
Microsoft Graph — the runspec-windows Graph runnables (Teams, OneDrive,
and Graph-based mail/calendar) authenticate as you via the device-code flow
(no client secret). One-time setup:
- Register an app in your Entra (Azure AD) tenant (App registrations → New registration).
- Authentication → "Allow public client flows" → Yes.
- API permissions → Microsoft Graph → Delegated: add
User.Read,Mail.Read,Chat.Read,Calendars.Read,Files.Read.All; grant consent (Chat.Readtypically needs a tenant admin). -
Copy the Application (client) ID and Directory (tenant) ID into
%APPDATA%\runspec-windows\graph.toml(the client id is not a secret):client_id = "00000000-0000-0000-0000-000000000000" tenant = "yourcompany.onmicrosoft.com" -
Run
graph-loginonce (it prints a URL + code); the token caches and refreshes silently.
For a personal Microsoft account, register the app for personal accounts
and set tenant = "consumers" (or "common").