Gateway Schedules
Declarative recurring agent → channel deliveries in gateway.yaml, with matching add/list/remove CLI
Run any agent on a cron / interval / one-shot and post the reply to a channel — from a few lines of YAML, with no Python.
Quick Start
- YAML — simplest form
Create
gateway.yamlwith a singleschedules:entry:yaml schedules: morning-brief: agent: personal cron: "0 8 * * *" # or: every: 24h / at: 2026-01-01T09:00 deliver: channel: telegram channel_id: ${OWNER_CHAT_ID} prompt: "Summarise my calendar and unread priorities for today."Start the gateway — the schedule loads at boot and fires through the shared tick + delivery loop:
bash praisonai gateway start --config gateway.yaml - CLI — manage schedules from the terminal
bash praisonai gateway schedule add morning-brief \ --agent personal \ --cron "0 8 * * *" \ --channel telegram \ --channel-id 12345 \ --prompt "Summarise my calendar and unread priorities for today." praisonai gateway schedule list praisonai gateway schedule remove morning-brief - Agent-centric — wire the agent in Python
The schedule references an agent by id; define that agent so the gateway can run it on each fire:
python from praisonaiagents import Agent agent = Agent( name="personal", instructions="Summarise the user's calendar and unread priorities each morning.", )
How It Works
The scheduler fires on the trigger, runs the agent, and routes the reply through the same channel-bot send path as inbound hooks.
| Concept | YAML key | CLI flag |
|---|---|---|
| Schedule name | map key | positional name |
| Agent to run | agent | --agent |
| Prompt per fire | prompt | --prompt |
| Cron trigger | cron | --cron |
| Interval trigger | every | --every |
| One-shot trigger | at | --at |
| Delivery platform | deliver.channel | --channel |
| Delivery target id | deliver.channel_id | --channel-id |
| Go/no-go gate | pre_run | --pre-run |
| Active? | enabled | — |
Choosing a Trigger — cron vs every vs at
Exactly one of cron / every / at must be set on each schedule.
| Trigger | Use for | Example |
|---|---|---|
cron | Recurring at wall-clock times | "0 8 *" (daily 8am) |
every | Fixed interval from last fire | "24h", "30m", "10s" |
at | One-shot at a specific timestamp | "2026-01-01T09:00" |
Schedule Reference
Every field on a schedules: entry.
| Option | Type | Default | Description | |
|---|---|---|---|---|
agent | str | — (required) | Agent id to run on each fire. | |
prompt | str | — (required) | Prompt the agent runs on each fire. | |
cron | `str \ | None` | None | Cron expression ("0 8 *"). Exactly one of cron / every / at. |
every | `str \ | None` | None | Interval ("24h", "30m", "10s", or raw seconds). Exactly one of cron / every / at. |
at | `str \ | None` | None | One-shot ISO timestamp ("2026-01-01T09:00"). Exactly one of cron / every / at. |
deliver | `DeliveryTarget \ | None` | None | Delivery target — omit for run-only jobs. |
pre_run | `str \ | None` | None | Optional cheap go/no-go command run before the model turn. "nothing to do" skips with no tokens spent and no delivery. |
enabled | bool | True | Whether the schedule is active. |
Zero triggers raises schedule requires exactly one of 'cron', 'every' or 'at'; more than one raises schedule accepts only one of 'cron', 'every' or 'at'.
Delivery
deliver: reuses the same channel-bot send path as inbound hooks — omit it for a run-only job with no channel post.
| Option | Type | Default | Description | |
|---|---|---|---|---|
channel | str | — (required) | Platform name (telegram, discord, slack, …). | |
channel_id | str | "" | Platform chat / channel id. | |
thread_id | `str \ | None` | None | Optional thread id within the channel. |
continuable | bool | True | One-way notification vs. resumable opener. Default lets a reply in the same chat resume the conversation. |
schedules:
digest:
agent: personal
every: 6h
deliver:
channel: slack
channel_id: "#daily"
continuable: false # one-way alert, no conversation thread
prompt: "Post the latest metrics digest."Pre-run Gate
Set pre_run to a cheap shell/python command that runs before the model turn — if it outputs nothing to do, the run is skipped with no LLM call and no delivery.
schedules:
newsletter:
agent: writer
cron: "0 9 * * 1"
pre_run: "test -f /tmp/newsletter.md"
deliver:
channel: telegram
channel_id: ${OWNER_CHAT_ID}
prompt: "Draft this week's newsletter from /tmp/newsletter.md."Idempotency & Hot-Reload
Each schedule gets a stable id cfg-<sha1(gateway-schedule:{name})[:12]> derived from its YAML key, so re-loading the same config upserts instead of duplicating.
- Idempotent on stable id — the SHA1-prefixed id is deterministic from the YAML key; boots and hot-reloads never accumulate duplicates.
last_run_atis carried forward on upsert, so an interval or one-shot job is not re-fired after a restart or edit.- Removed schedules stop firing — a schedule deleted from the YAML is pruned on the next reload (only
cfg-prefixed jobs are ever pruned). - Chat-created jobs are untouched — jobs created in-chat via the agent-callable
scheduletool carry random ids and are never reconciled.
GatewayServer._load_declarative_schedules(config) runs right before the scheduler tick starts and again on every hot-reload, so additions, edits and removals all take effect without a process restart.
Failure Semantics
The gateway loads schedules best-effort so a single bad entry never blocks startup.
- Malformed entries are skipped, not fatal — a single bad entry logs
Skipping invalid schedule 'x': ...and startup continues. A warning also surfaces at load time. - Refuses to clobber a broken config — CLI
add/removewill not overwrite agateway.yamlit could not read as a YAML mapping; the original file is preserved and the command exits1. - Exit codes — passing zero or more than one trigger flag, or a missing
--agent/--prompt, exits1.
Tick Resilience
The scheduler tick loop recovers from errors on its own, so one bad tick never stops your schedules.
sequenceDiagram
participant Loop as Tick loop
participant Policy as RunPolicy refresh
participant Tick as tick_all()
participant Log as Logger
Loop->>Policy: Rebuild policy from config
Policy-->>Loop: policy
Loop->>Tick: Run due jobs
alt tick ok
Tick-->>Loop: results
Note over Loop: failures = 0, sleep(interval)
else tick raises (e.g. EMFILE on store lock)
Tick-->>Log: Scheduler tick failed (N in a row)
Note over Loop: failures += 1, sleep with backoff
end
Note over Loop: Next tick runs, task stays alive| Scenario | Behaviour |
|---|---|
Store lock open() fails with EMFILE | Logged with traceback; next tick retried with backoff |
scheduler: block hot-reloaded as a non-mapping (e.g. a list) | Warning logged; fail-closed defaults used; loop keeps ticking |
| Any other exception in a tick | Logged with traceback; retried with backoff |
Gateway shutdown (asyncio.CancelledError) | Loop exits cleanly |
- Backoff — after a failed tick the loop sleeps
min(interval * 2 ** min(failures, 10), 300)seconds, so retries never wait longer than 5 minutes. - Reset on success — the first successful tick resets
failuresto0and the loop goes back to the normalinterval. - Visible, not silent — each failure logs
Scheduler tick failed (N in a row)with a full traceback, so a stuck store shows up in the logs even while chat channels and/healthlook fine.
At 08:00 the
morning-briefschedule is due, but the store lock file briefly hitsEMFILE. The gateway logs the error, waitsinterval × 2seconds, retries, and the brief arrives a little late. No restart is needed.
CLI Reference
add
praisonai gateway schedule add <name> \
--agent <agent-id> \
--prompt "..." \
(--cron "0 8 * * *" | --every 24h | --at 2026-01-01T09:00) \
[--channel telegram] [--channel-id 12345] \
[--pre-run "..."] \
[--config gateway.yaml]| Flag | Required | Description |
|---|---|---|
name (positional) | yes | Schedule name, e.g. morning-brief. |
--agent | yes | Agent id to run. |
--prompt | yes | Prompt the agent runs on each fire. |
--cron / --every / --at | one exactly | Trigger. |
--channel | no | Delivery platform. |
--channel-id | no | Delivery chat/channel id. |
--pre-run | no | Optional shell go/no-go gate. |
--config | no | Path to gateway.yaml (default gateway.yaml). |
list
praisonai gateway schedule list [--config gateway.yaml]Prints one line per schedule with agent, trigger, and delivery target:
Schedules in gateway.yaml:
morning-brief agent=personal cron=0 8 * * * deliver=telegram:12345remove
praisonai gateway schedule remove <name> [--config gateway.yaml]Real User-Interaction Flow
At 8am Telegram chat
${OWNER_CHAT_ID}gets a summary of the user’s calendar and unread priorities. The user can reply in the same chat — becausecontinuable: true— and thepersonalagent resumes the conversation.
Common Patterns
Daily brief to Telegram (cron)
schedules:
morning-brief:
agent: personal
cron: "0 8 * * *"
deliver:
channel: telegram
channel_id: ${OWNER_CHAT_ID}
prompt: "Summarise my calendar and unread priorities for today."Hourly health-check poll to Slack (every)
schedules:
health-poll:
agent: ops
every: 1h
deliver:
channel: slack
channel_id: "#ops"
prompt: "Check service health and report any anomalies."One-shot launch reminder (at)
schedules:
launch-reminder:
agent: personal
at: "2026-01-01T09:00"
deliver:
channel: telegram
channel_id: ${OWNER_CHAT_ID}
prompt: "Remind me that the launch checklist is due today."Best Practices
Use a pre_run gate for expensive automations
A cheap pre_run command that prints nothing to do skips the model turn entirely — no tokens spent and no delivery when there’s nothing new.
Prefer every for jitter-tolerant polling, cron for wall-clock deliveries
Use every when the exact minute doesn’t matter (health polls); use cron when the delivery must land at a specific wall-clock time (an 8am brief).
Keep schedule names stable
The stable id derives from the YAML key — renaming a schedule creates a new job and drops the old one’s run-state.
Set continuable: false for one-way notifications
A bare alert that shouldn’t open a conversation thread should set continuable: false so a reply doesn’t resume an agent turn.
Related
The inbound HTTP trigger counterpart to this outbound scheduled surface.
Schedule CLIThe standalone praisonai schedule CLI — store poller, tick, and job management.
Friendly aliases for scheduled delivery.
Gateway OverviewGateway architecture and how channels, agents, and routing connect.
Scheduled Run PolicyRunPolicy for scheduled jobs.
Scheduler Multi-TenantPer-user isolation for scheduled jobs.