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.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.- Python
- YAML
- 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
Thetools: 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
Thedeliver 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.
telegram for discord, slack, or whatsapp — the grammar is identical.
Deliver back to the origin channel
Usedeliver="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.
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.
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.- CLI
- Tool
- Python
The seed contract
Given a successful delivery oftext 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
Pure status alerts
Pure status alerts
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.Sub-minute ticks
Sub-minute ticks
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 and digests (leave it on)
Recurring briefs and digests (leave it on)
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 toSchedulerDelivery, 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 exactlyNO_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.
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 existingDeliveryRouter, 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
Fire-and-forget notice
--no-continuable when a reply should not resume the job — the reply starts a fresh session, matching pre-#3449 behaviour.
Best Practices
Use an explicit channel ID for scheduled jobs
Use an explicit channel ID for scheduled jobs
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.Install the bot extra to enable delivery
Install the bot extra to enable delivery
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.Reuse one scheduler instance per job
Reuse one scheduler instance per job
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.
Pick the surface that matches how you deploy
Pick the surface that matches how you deploy
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.Related
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 commandsGateway Inbound Hooks
Shares the same delivery target format

