Skip to main content
Set deliver= on a scheduled agent and each result is pushed to a chat target — no gateway required.
New: every delivered brief is now a continuable conversation by default — a user’s reply in the same chat resumes the job’s turn with the brief in context. Opt out per-job with --no-continuable for pure notifications. See Continuable Delivery.
“Run this agent every hour and text me the result on Telegram.” That used to mean wiring up the scheduler and the full BotOS gateway. Now it’s one parameter — the scheduler talks to the shared DeliveryRouter directly.
If praisonai-bot isn’t installed, delivery logs a single warning and no-ops. The scheduled run itself never fails because delivery failed.

Quick Start

1

Deliver to a specific chat

2

Deliver to the platform home channel

3

Reply back to the origin chat

4

Async scheduler


Three Ways to Set the Target

The same delivery token works from Python, YAML, and the CLI.
every: is a new alias for interval: in the YAML schedule: block — both accept hourly, daily, weekly, */30m, or raw seconds.

Tools in scheduled YAML

The tools: list inside agents.yaml resolves through the same ToolResolver that praisonai run agents.yaml uses — so any tool name the CLI accepts also works when the file is scheduled via AgentScheduler.from_yaml() or AsyncAgentScheduler.from_yaml(). See Tool Resolver.
Before PraisonAI #3420, the scheduled YAML loader only recognised the hardcoded names search_tool / InternetSearchTool — every other tool was silently dropped and the agent ran with tools=[]. The scheduler now uses the canonical ToolResolver, keeping CLI, Python, and scheduled surfaces identical.

Which surface fits which scenario?


Delivery Target Tokens

The deliver value is parsed by DeliveryTarget.parse() — the same serialisable model reused from the existing delivery machinery.
origin now works on the lightweight scheduler path when the job has a persisted origin (ScheduleJob.origin — set automatically when a job is created from a bot/webhook request). It resolves to the same channel/thread the request came in on, no gateway required. If the job has no persisted origin, the lightweight path logs a warning and skips delivery. Only all still needs the full gateway — it enumerates every registered bot.

Thread semantics per platform

The third :thread_id segment threads the outbound message. DeliveryRouter.resolve() returns (platform, channel_id, thread_id) and deliver() passes thread_id into bot.send_message(...). Adapters without a thread_id kwarg are unaffected — a guard introspects the adapter first.
Swap telegram for discord, slack, or whatsapp — the grammar is identical.

Deliver back to the origin channel

Use deliver="origin" when the schedule was created from a chat and you want the recurring result to land back in that same chat — without wiring up a specific platform:channel_id token.
How origin resolution works: SchedulerDelivery.origin_from_config() normalises the origin whether it is a live DeliveryTarget or its persisted dict form (from to_dict()), so scheduled jobs restored from disk resolve correctly too.
deliver="origin" still logs a warning and no-ops if the job was created without an origin channel — for example, a job created purely from Python code with no chat context. In that case, use an explicit platform:channel_id token.

Continuable Delivery

A delivered brief is a conversation opener, not a dead-end. Reply in the same chat and the agent picks up where the brief left off — with the delivered text already in context. Zero configuration, on by default. Before / after #3449:

Quick start — nothing to do

Continuable delivery is default-on. Every scheduled job with a delivery target already resumes its conversation on reply:

Opt out for fire-and-forget alerts

Some deliveries are pure notifications (“build finished”, “backup ok”) — a reply should stay a fresh turn, not resume a maintenance job. Turn seeding off with a single flag:
cron: schedules (like cron:0 3 * * * below) honour wall-clock time-of-day and survive restarts (introduced in PR #3527). Install croniter (pip install croniter) to enable this — without it the wrapper falls back to a coarse interval and warns once. See Wall-clock cron.

The seed contract

Given a successful delivery of text to channel:channel_id, the gateway seeds a resumable session:
The continuable field is a declarable contract. Core (praisonaiagents/scheduler/models.py) owns the field and its round-trip; the actual seeding runs in the praisonai-bot gateway. Deployments running the scheduler without the bot wrapper (direct scheduler → agent, no gateway) still round-trip the field and honour the opt-out, but there is no session to seed.

Configuration options

Serialisation. DeliveryTarget.to_dict() persists continuable only when it is False — the default is implied by absence, so existing on-disk schedule stores are byte-for-byte unchanged. from_dict() treats a missing field as True, so restored legacy jobs immediately gain the new behaviour on next fire.

When to opt out

Backup finished, deploy succeeded, monitoring “ok” heartbeat. There is no conversation to have — the reply, if any, is an unrelated command. Use --no-continuable so the chat context stays fresh.
A broadcast to every home channel implies the recipients aren’t a single continuing conversation. Seeding runs once per destination channel:channel_id, so a shared/group chat resumes the last fire’s context, not per-participant. If replies in the shared chat are noise, prefer --no-continuable.
A tight loop that seeds on every fire would overwrite the session before the user can read it. Prefer --no-continuable (or a coarser schedule) for high-frequency jobs.
Recurring briefs, digests, standing reports, sensor summaries — the reader almost always wants to reply to the thing they just read. Leave continuable on.

How It Works

The scheduler runs the agent, then hands the result to SchedulerDelivery, which resolves the token and sends through the shared DeliveryRouter. SchedulerDelivery is built once per scheduler and reused across runs, so the router’s idempotency cache and rate limiters persist between ticks. After a successful delivery, a gateway-hosted job also seeds a resumable session so a reply resumes the conversation — see Continuable Delivery.

Intentional Silence

A scheduled run whose entire output is exactly NO_REPLY, [SILENT], or SILENT skips delivery — nothing is pushed to the chat target. The run is still recorded as succeeded in scheduler history and metrics.
The noisy NO_REPLY control token is never posted to the channel — the tick just doesn’t produce any message. The marker check calls is_intentional_silence_response from praisonaiagents.bots.silence — the same primitive the chat-bot path uses. All three unattended paths honour it via the shared _BaseAgentScheduler._should_suppress_delivery helper: AgentScheduler._deliver_result (sync lightweight), AsyncAgentScheduler._deliver_result (async lightweight, wired in PraisonAI #3420), and the full-gateway ScheduledAgentExecutor.
Unlike the chat-bot path — where silence is opt-in via allow_silence: true — the scheduled delivery path honours silence markers unconditionally. An unattended monitor should never post the raw control token to a channel, so there is no allow_silence toggle for the scheduler.

Reliability Guarantees

Delivery reuses the existing DeliveryRouter, so scheduled sends inherit the gateway’s guarantees. On the gateway-hosted scheduled path, delivery is enqueued into a durable SQLite outbox at ~/.praisonai/state/gateway_outbox.sqlite before it reaches the router. Its UNIQUE idempotency key survives a process restart, so the exact crash window a scheduler must survive (fire → deliver → crash → restart → re-fire) is deduplicated without a double-post. A pre-crash send that never landed stays retryable and is delivered at-least-once on the next tick. If the outbox cannot be built (missing dependency or permission error) the path falls back to the router’s in-process LRU exactly as before.

Common Patterns

Deliver from a blueprint

deliver= on from_blueprint overrides the blueprint’s default delivery target.

Deliver to a thread

Attachments produced by the job (media returned by the agent) now travel into the same thread — parity with text delivery is preserved on Slack, Telegram, and Discord. See Outbound Media Delivery for the thread grammar.

Fire-and-forget notice

Use --no-continuable when a reply should not resume the job — the reply starts a fresh session, matching pre-#3449 behaviour.

Best Practices

telegram:123456 targets a fixed chat and works without any request context.origin now works on the lightweight scheduler path when the job has a persisted origin (ScheduleJob.origin, set automatically when the job is created from a bot/webhook request) — it resolves to the same channel/thread the request came in on, no gateway required. all still needs the full gateway. For jobs created outside a bot request (no persisted origin), prefer an explicit platform:channel_id token.
Delivery needs the praisonai-bot package. Without it, the scheduled run still executes — only the push is skipped, with one warning. Install the bot extra to turn delivery on.
The router’s idempotency cache and rate limiters live on the scheduler’s delivery helper. Keep a single scheduler running so repeated identical results to the same target are deduplicated instead of re-sent.
Use Python inside an app, YAML for a config-file deploy, and the CLI for one-off terminal jobs — all three accept the same deliver token grammar.

Async Agent Scheduler

Run agents on a recurring schedule with async execution

Pre-Run Gate

Skip ticks when a cheap check says nothing to do

Schedule CLI

The --deliver / -d flag and other schedule commands

Gateway Inbound Hooks

Shares the same delivery target format