Skip to main content
Run a cheap shell check before each scheduled tick — skip the model turn (and any delivery ping) when there’s nothing to do.

Quick Start

1

YAML (simplest)

Add pre_run to your agents.yaml schedule block. The agent only runs when the script exits 0 with output.
schedule:
  every: 300
  pre_run: scripts/new_mail.sh
  message: "Summarise these new emails and flag anything important."
2

Python

Set pre_run directly on a ScheduleJob.
from praisonaiagents import Agent
from praisonaiagents.scheduler import ScheduleJob, Schedule

agent = Agent(
    name="InboxWatcher",
    instructions="Summarise new emails and flag anything important.",
)

job = ScheduleJob(
    name="inbox-watch",
    schedule=Schedule(kind="every", every_seconds=300),
    pre_run="scripts/new_mail.sh",
    message="Summarise these new emails and flag anything important.",
)
3

CLI

Pass --pre-run when adding a scheduled job.
praisonai schedule add "inbox-watch" \
  -s "*/5m" \
  -m "Summarise new emails" \
  --pre-run "scripts/new_mail.sh" \
  --deliver telegram

How It Works

The gate runs in asyncio.to_thread so a slow check does not block other scheduled ticks. If the gate itself raises an exception, the executor falls back to running the agent (defensive default).

Configuration Options

ScheduleJob fields

FieldTypeDefaultDescription
pre_runOptional[str]NoneShell command run before each tick. Set via YAML schedule.pre_run or --pre-run
conditionOptional[str]NoneAdvisory natural-language label (round-tripped, not enforced by the default gate)

ScheduledAgentExecutor parameter

ParameterTypeDefaultDescription
condition_resolverNone | False | callableNoneNone = auto shell gate when pre_run set; False = gating disabled; callable (job) → gate | None = custom resolver

ShellConditionGate options

OptionTypeDefaultDescription
timeoutfloat30.0Seconds before the gate is treated as a skip
_MAX_CONTEXT_CHARSint8000Cap on captured stdout appended as context
_MAX_REASON_CHARSint500Cap on stderr surfaced in the skip reason

Gate decision table

Gate command exitstdoutDecisionWhat happens
0non-emptyrun=True, context=stdoutModel runs, stdout appended to message
0emptyrun=True, context=NoneModel runs with original message only
non-zerorun=FalseTick recorded as skipped — no tokens, no delivery
timeout (>timeouts)run=FalseSame as skipped
gate raisesfalls back to run=TrueModel runs (defensive)
pre_run executes an arbitrary host shell command on every tick. It is not exposed through the agent-callable schedule_add tool — accepting it from an LLM would allow a prompt-injected agent to persist server-side command execution. Configure pre_run only via CLI, YAML, or Python.

Pre-Run Gate vs RunPolicy

Two complementary gates apply on every tick when both are configured:
GatePurposeWhen to configure
Pre-run gate (pre_run)Efficiency — should a run happen at all?When most ticks have nothing to do
RunPolicySafety — what is an unattended run allowed to do?Always in production unattended runs

Common Patterns

Inbox watcher

Only fire when new mail arrives — cuts 288 daily runs to just the count of mail bursts.
schedule:
  every: 300
  pre_run: scripts/new_mail.sh
  message: "Summarise these new emails and flag anything important."
scripts/new_mail.sh exits 0 and prints new messages when mail is present; exits non-zero when the inbox is quiet.

CI babysitter

Alert a channel only when a build breaks.
praisonai schedule add "ci-watch" \
  -s "*/5m" \
  -m "Summarise the CI failure and suggest a fix" \
  --pre-run "scripts/ci_failed.sh" \
  --deliver telegram

Custom Python gate

Use a callable resolver for richer logic than a shell exit code.
from praisonaiagents import Agent
from praisonaiagents.scheduler import ScheduleJob, Schedule
from praisonai.scheduler.executor import ScheduledAgentExecutor

class MyPythonGate:
    def check(self, job):
        import requests
        r = requests.get("https://api.example.com/has-work")
        return r.json().get("pending", 0) > 0

agent = Agent(name="WorkProcessor", instructions="Process pending work items.")

job = ScheduleJob(
    name="work-processor",
    schedule=Schedule(kind="every", every_seconds=60),
    message="Process all pending work items.",
)

executor = ScheduledAgentExecutor(
    runner=runner,
    agent_resolver=lambda _: agent,
    condition_resolver=lambda job: MyPythonGate(),
)

Best Practices

The gate runs on every tick. Aim for under 1 second — a fast file check, an API ping, or a database row count. Avoid heavy computation.
# Good — fast local check
scripts/new_mail.sh

# Avoid — slow network calls without caching
scripts/fetch_all_data_and_analyse.sh
The default timeout=30.0 is conservative. A runaway gate stalls that tick’s slot. Set it to match your gate’s expected worst case.
from praisonai.scheduler.condition_gate import ShellConditionGate

gate = ShellConditionGate(timeout=5.0)
The same check that gates the run can feed it the data — the gate’s stdout is automatically appended to the message.
#!/bin/bash
# scripts/new_mail.sh
# Prints new mail subjects if any, exits non-zero if inbox empty
NEW=$(mail -H 2>/dev/null | head -20)
if [ -z "$NEW" ]; then exit 1; fi
echo "$NEW"
The agent then receives: "Summarise these new emails... <new mail subjects>".
The pre_run string is stored verbatim in the job record. Use environment variables or a secrets manager instead of inline credentials.
# Good — use environment variable
scripts/check_api.sh

# Avoid — secret visible in job record
curl -H "Authorization: Bearer mysecrettoken" https://api.example.com/check
The agent-callable schedule_add tool deliberately does not accept pre_run. This prevents a prompt-injected agent from persisting arbitrary shell commands on the host. Always configure pre_run through the CLI, YAML, or Python code.
A gate returning non-zero records the tick as skipped (not failed) — this is expected normal operation when there’s nothing to do. Check your run history with praisonai schedule logs to see skip rates.

Async Agent Scheduler

The scheduler this gate plugs into

Schedule CLI

CLI surface — where --pre-run and --condition are configured