Skip to main content
from praisonaiagents import Agent
from praisonaiagents.tools import schedule_add, schedule_list, schedule_remove

agent = Agent(
    name="Scheduler",
    instructions="Add and list scheduled reminders for the user.",
    tools=[schedule_add, schedule_list, schedule_remove],
)
agent.start("Remind me daily at 9:00 to review the inbox")
The user describes a recurring reminder; the agent registers a schedule job via schedule tools.
Built-in — no extra dependencies required. Schedule tools are included in the core praisonaiagents package.
Schedule tools let your agents self-schedule reminders, recurring tasks, and one-shot jobs — all via simple tool calls. Optionally gate each tick with a cheap shell check via pre_run so expensive model turns only happen when there’s real work to do. No changes to the Agent class are needed.

Quick Start

1

Simple Usage

from praisonaiagents import Agent
from praisonaiagents.tools import schedule_add, schedule_list, schedule_remove

agent = Agent(
    name="assistant",
    instructions="You can set reminders and schedules for the user.",
    tools=[schedule_add, schedule_list, schedule_remove],
)

agent.start("Remind me to check email every morning at 7am")
2

With Configuration

from praisonaiagents.tools import schedule_list

print(schedule_list())
The agent will call schedule_add with the appropriate schedule expression, and the job will be persisted to disk.

Available Tools

schedule_add

Add a new scheduled job.
ParameterTypeRequiredDescription
namestrYesHuman-readable name (e.g. "morning-email-check")
schedulestrYesWhen to run (see Schedule Expressions)
messagestrNoPrompt or reminder text to deliver when triggered
Returns: Confirmation string with the job id.

schedule_list

List all scheduled jobs. Takes no parameters. Returns: Formatted string listing every job with id, name, schedule, status, and message.

schedule_remove

Remove a scheduled job by name.
ParameterTypeRequiredDescription
namestrYesName of the schedule to remove
Returns: Confirmation or not-found message.

Schedule Expressions

FormatExampleDescription
Keyword"hourly", "daily"Predefined intervals
Interval"*/30m", "*/6h", "*/10s"Custom interval (minutes, hours, seconds)
Cron"cron:0 7 * * *"5-field cron expression
One-shot"at:2026-03-01T09:00:00"ISO 8601 timestamp
Relative"in 20 minutes"Relative to now
Seconds"3600"Raw seconds

Examples

Recurring Schedule

from praisonaiagents import Agent
from praisonaiagents.tools import schedule_add, schedule_list, schedule_remove

agent = Agent(
    name="news-bot",
    instructions="""You help users stay informed.
    When asked, create schedules for news summaries.
    Use schedule_add with cron expressions for precise timing.""",
    tools=[schedule_add, schedule_list, schedule_remove],
)

# Agent will create: schedule_add("morning-news", "cron:0 7 * * *", "Summarize AI news")
agent.start("Send me an AI news summary every morning at 7am")

One-Shot Reminder

# Agent will create: schedule_add("meeting-prep", "in 20 minutes", "Prepare for standup")
agent.start("Remind me in 20 minutes to prepare for standup")

List and Manage

# Agent will call schedule_list() and schedule_remove("old-task")
agent.start("Show me my schedules and remove 'old-task'")

Using String Tool Names

agent = Agent(
    name="scheduler",
    tools=["schedule_add", "schedule_list", "schedule_remove"],
)

Storage

Jobs are persisted to ~/.praisonai/config.yaml under the schedules key by default via ConfigYamlScheduleStore. The store is:
  • Thread-safe for multi-agent scenarios
  • Atomic writes (tmp + rename) to prevent corruption
  • Auto-created on first use
  • Auto-migrates legacy jobs.json data on first load

Custom Store (ScheduleStoreProtocol)

Swap the default file store for any backend that implements ScheduleStoreProtocol:
from praisonaiagents.scheduler import ScheduleStoreProtocol

class MyDatabaseStore:
    """Any object with these methods works."""
    def add(self, job): ...
    def get(self, job_id) -> Optional[Any]: ...
    def list(self, agent_id=None) -> list: ...
    def update(self, job): ...
    def remove(self, job_id) -> bool: ...
    def get_by_name(self, name) -> Optional[Any]: ...
    def remove_by_name(self, name) -> bool: ...

assert isinstance(MyDatabaseStore(), ScheduleStoreProtocol)  # ✅
Inject it at startup so all agent schedule_add/list/remove calls use your store:
from praisonaiagents.tools.schedule_tools import set_store

my_store = MyDatabaseStore()
set_store(my_store)  # All schedule tools now use this store
PraisonAIUI and BotOS use the same config.yaml store. You can also call set_store() to inject any custom backend.

Schedule Runner

The ScheduleRunner checks which jobs are due for execution:
from praisonaiagents.scheduler import ScheduleRunner, ConfigYamlScheduleStore

store = ConfigYamlScheduleStore()
runner = ScheduleRunner(store=store)

# Get jobs that are due right now
due_jobs = runner.get_due_jobs()

for job in due_jobs:
    print(f"Due: {job.name}{job.message}")
    runner.mark_run(job)  # Updates last_run_at

Hook Events

Schedule lifecycle events are available via the hook system:
EventWhen
SCHEDULE_ADDA new schedule is created
SCHEDULE_REMOVEA schedule is deleted
SCHEDULE_TRIGGERA scheduled job fires

Execution History

Every scheduled job execution is logged as a RunRecord for auditing:
from praisonaiagents.scheduler import FileScheduleStore

store = FileScheduleStore()

# Get last 10 executions for a job
history = store.get_history("job-abc123", limit=10)

for run in history:
    print(f"{run.job_name}: {run.status} ({run.duration:.1f}s)")
    if run.error:
        print(f"  Error: {run.error}")
FieldTypeDescription
job_idstrJob that was executed
job_namestrHuman-readable job name
statusstr"succeeded", "failed", or "skipped" (when a pre_run gate returned run=False)
resultstrAgent output (truncated)
errorstrError message if failed
durationfloatExecution time in seconds
deliveredboolWhether result was delivered to channel
timestampfloatEpoch timestamp

Executing Scheduled Jobs

Schedule tools create and persist jobs, but to actually execute them when they’re due, use ScheduleLoop:
from praisonaiagents.scheduler import ScheduleLoop

def handle_job(job):
    print(f"🔔 {job.name}: {job.message}")

loop = ScheduleLoop(on_trigger=handle_job, tick_seconds=30)
loop.start()
See Background Tasks — ScheduleLoop for the full API and combined examples with BackgroundRunner.

Pre-Run Condition Gate

Gate a scheduled tick on a cheap shell check so no model tokens are spent when there’s nothing to do.
1

Add pre_run to a schedule in bot.yaml

# bot.yaml
schedules:
  - name: new-issue-triage
    schedule: "*/5m"
    message: "Triage any new issues from the queue."
    pre_run: "gh issue list --state open --json number,title --search 'created:>1h ago'"
2

Every tick, PraisonAI evaluates pre_run before spending tokens

Exit codestdoutOutcome
0emptyJob runs with original message; no context added
0non-emptyJob runs; stdout appended as context (capped at 8 000 chars)
non-zeroJob skipped; truncated stderr in reason (max 500 chars)
timed out (>30 s)Job skipped; process group killed; reason: pre-run gate timed out
pre_run is a cost gate (decides whether to run). It is not a safety gate (RunPolicy, which decides what a run may do). Use both when you need both.

Real-World Examples

Only triage when new issues exist:
pre_run: "gh issue list --state open --search 'created:>10m ago' --json title"
Only summarise inbox when there’s unread mail:
pre_run: "test -s $HOME/.mail/inbox/unread"
Guard against off-hours runs (Monday–Friday, 09:00–18:00):
pre_run: "test $(date +%u) -lt 6 && test $(date +%H) -ge 9 && test $(date +%H) -lt 18"

Custom Condition Gate

Any object implementing JobConditionProtocol can replace the default shell gate — a Python callable, an MCP probe, a database check.
from praisonaiagents.scheduler.protocols import GateResult, JobConditionProtocol
from praisonai.scheduler.executor import ScheduledAgentExecutor

class DatabaseGate:
    def should_run(self, job) -> GateResult:
        rows = pending_rows()
        if rows == 0:
            return GateResult(run=False, reason="no pending rows")
        return GateResult(run=True, context=f"{rows} rows waiting")

executor = ScheduledAgentExecutor(
    runner=runner,
    agent_resolver=lambda agent_id: gateway.get_agent(agent_id),
    condition_resolver=lambda job: DatabaseGate(),
)
Pass condition_resolver=False to disable gating entirely. The default resolver automatically activates ShellConditionGate for any job that has a pre_run value.

BotOS Integration

When using BotOS (multi-platform bot orchestrator), scheduled jobs execute automatically — no ScheduleLoop needed. BotOS runs its own 30-second schedule tick alongside all bots:
  • Agents create jobs via schedule_add during conversations
  • BotOS detects due jobs every 30 seconds
  • The originating agent processes the job message
  • Results are delivered back to the originating platform (Telegram, Discord, etc.)
# bot.yaml — schedules work out of the box
agent:
  name: assistant
  tools:
    - schedule_add
    - schedule_list
    - schedule_remove

channels:
  telegram:
    token: ${TELEGRAM_BOT_TOKEN}
praisonai bot start --config bot.yaml
# Agent can now self-schedule and BotOS executes + delivers results

Architecture

Schedule tools follow PraisonAI’s core principles:
  • Agent-centric — tools, not Agent parameters
  • Lazy-loaded — zero import cost until used
  • Protocol-drivenScheduleStoreProtocol makes stores swappable
  • No Agent bloat — the Agent class is unchanged
  • Thread-safe — safe for multi-agent workflows
  • Pluggableset_store() lets any backend replace the default file store

See Also

Background Tasks

Sync wrappers, ScheduleLoop, and combined recipes

Scheduler CLI

24/7 autonomous agent scheduling via CLI

Best Practices

Cron expressions give exact control over scheduling - prefer them for production use.
Add logging to scheduled agent tasks so you can verify they ran and diagnose failures.
Use 1-minute intervals during testing, then switch to production schedules before deployment.
Scheduled jobs should catch exceptions and report errors rather than silently failing.
If a schedule only has work when some external state changes (new emails, new PRs, a queue with pending rows), put the cheap check in pre_run. Model tokens are spent only for ticks that actually have work to do.

Custom Tools

Build your own agent tools

Tools Overview

Browse PraisonAI tool documentation