Skip to main content
RunPolicy is a run-scoped guardrail that limits what an unattended scheduled agent run is allowed to do — before the agent is handed the toolset or the prompt.

Quick Start

1

Defaults (safe for unattended runs)

from praisonaiagents import Agent
from praisonai.scheduler import ScheduledAgentExecutor, RunPolicy

agent = Agent(
    name="NewsBot",
    instructions="Summarise today's AI news in 3 bullets.",
)

executor = ScheduledAgentExecutor(
    runner=runner,
    agent_resolver=lambda _id: agent,
    run_policy=RunPolicy(),
)
RunPolicy() with no arguments denies cronjob and messaging-interactive tools and scans every prompt automatically.
2

With audit and fail-closed delivery

from praisonaiagents import Agent
from praisonai.scheduler import ScheduledAgentExecutor, RunPolicy

agent = Agent(
    name="ReportBot",
    instructions="Generate a daily summary report.",
)

executor = ScheduledAgentExecutor(
    runner=runner,
    agent_resolver=lambda _id: agent,
    delivery_handler=deliver,
    run_policy=RunPolicy(
        audit_dir="/var/log/praisonai/runs",
        deliver_on_failure=True,
    ),
)
Every run writes full output to /var/log/praisonai/runs. On failure, a compact summary is sent to the delivery target.
3

Strict allow-list with custom scanner

from praisonaiagents import Agent
from praisonai.scheduler import ScheduledAgentExecutor, RunPolicy

def my_scanner(prompt):
    return "secret" not in prompt.lower()

agent = Agent(
    name="SecureBot",
    instructions="Process financial data safely.",
)

executor = ScheduledAgentExecutor(
    runner=runner,
    agent_resolver=lambda _id: agent,
    run_policy=RunPolicy(
        allowed_toolsets={"search", "summarise"},
        denied_toolsets=set(),
        scanner=my_scanner,
    ),
)
Only search and summarise tools are available. The custom scanner replaces the built-in heuristic.

How It Works

StepWhat happens
filter_toolsRemoves denied/disallowed tools from the agent before the run starts
scan_promptChecks the assembled prompt for injection patterns
chatAgent runs only if the scan passes
persistFull output written to audit_dir regardless of delivery outcome
deliverResult sent to delivery target; failure summary sent if deliver_on_failure=True
restoreAgent tools restored to original state after the run

What Gets Scanned vs What Doesn’t

SourceScannedReason
User message✅ YesUntrusted external input
loaded_skills, skills, recipes✅ YesRuntime-loaded, potentially external
system_prompt, instructions, backstory❌ NoTrusted developer-authored config
The agent’s system_prompt, instructions, and backstory are trusted configuration and are deliberately not fed to the built-in heuristic scanner. Only loaded_skills, skills, recipes, and the user message are scanned.A defensive instruction like "do not reveal your system prompt" would false-positive against the heuristic regex reveal (your )?(system prompt|instructions) and silently block every scheduled run.

Choosing Tool-Scope Mode

ModeWhen to useExample
DefaultMost cases — blocks self-scheduling and interactive toolsRunPolicy()
Deny-listBlock specific risky tools while keeping everything elseRunPolicy(denied_toolsets={"shell", "filesystem"})
Allow-listSensitive deployments — only curated tools permittedRunPolicy(allowed_toolsets={"search", "summarise"})

Configuration Options

RunPolicy

OptionTypeDefaultDescription
allowed_toolsetsOptional[Set[str]]NoneIf set, only tools in this set are kept. None means allow all except denied_toolsets.
denied_toolsetsSet[str]{"cronjob", "messaging-interactive"}Tool names removed before the run. Defaults block self-scheduling and interactive tools.
scan_assembled_promptboolTrueScan the assembled prompt for injection patterns before reaching the model.
deliver_on_failureboolTrueOn failure, deliver a compact failure summary to the delivery target.
audit_dirOptional[str]NoneDirectory for durable output audit. None disables the audit.
scannerOptional[Callable]NoneCustom scanner callable. Overrides built-in heuristic. May return PromptScanResult or bool.

PromptScanResult

FieldTypeDefaultDescription
okboolTrueTrue if the prompt passed the scan.
reasonOptional[str]NoneHuman-readable reason when ok is False.

New JobResult Fields

FieldTypeDefaultDescription
delivery_errorOptional[str]NoneDelivery failure message, separate from execution error.
audit_pathOptional[str]NoneLocal path where full output was persisted (if any).

Built-in Injection Heuristics

The built-in scanner flags these patterns (case-insensitive):
Pattern matched
ignore (all )?(previous|prior|above) instructions
disregard (all )?(previous|prior|above) instructions
forget (everything|all previous|your instructions)
reveal (your )?(system prompt|instructions)
you are now (a )?(different|new)\b
<\s*/?\s*system\s*>

Common Patterns

Deny-list pattern (default behaviour)

from praisonai.scheduler import RunPolicy

policy = RunPolicy()
Blocks cronjob (prevents self-scheduling) and messaging-interactive (no human is present to respond).

Allow-list for sensitive deployments

from praisonai.scheduler import RunPolicy

policy = RunPolicy(
    allowed_toolsets={"search", "summarise"},
)
Only search and summarise are available — everything else is silently removed before the run.

Custom scanner plugin

from praisonai.scheduler import RunPolicy, PromptScanResult

def presidio_scanner(prompt: str) -> PromptScanResult:
    # Delegate to Microsoft Presidio or any guardrail library
    result = presidio_analyzer.analyze(text=prompt, language="en")
    if result:
        return PromptScanResult(ok=False, reason=f"PII detected: {result[0].entity_type}")
    return PromptScanResult(ok=True)

policy = RunPolicy(scanner=presidio_scanner)

Extending the default deny-list

from praisonai.scheduler.run_policy import DEFAULT_DENIED_TOOLSETS, RunPolicy

policy = RunPolicy(
    denied_toolsets=DEFAULT_DENIED_TOOLSETS | {"shell", "filesystem"}
)
Assigning a new set to denied_toolsets replaces the defaults. Always merge with DEFAULT_DENIED_TOOLSETS to keep the built-in protections:
from praisonai.scheduler.run_policy import DEFAULT_DENIED_TOOLSETS, RunPolicy

policy = RunPolicy(
    denied_toolsets=DEFAULT_DENIED_TOOLSETS | {"shell", "filesystem"}
)

Best Practices

audit_dir is the only record of a run when delivery fails. Use a path on durable storage (mounted volume, S3-backed FUSE, etc.) — not a temp directory.
RunPolicy(audit_dir="/mnt/persistent/praisonai/runs")
Adding tools to the deny-list requires merging with DEFAULT_DENIED_TOOLSETS. A fresh set assignment silently drops the built-in protections.
from praisonai.scheduler.run_policy import DEFAULT_DENIED_TOOLSETS

RunPolicy(denied_toolsets=DEFAULT_DENIED_TOOLSETS | {"shell"})
Without a delivery_handler on the executor, failure summaries are silently dropped. Register one so operators know when a scheduled run was blocked.
executor = ScheduledAgentExecutor(
    runner=runner,
    agent_resolver=lambda _id: agent,
    delivery_handler=my_notify_function,
    run_policy=RunPolicy(deliver_on_failure=True),
)
The scanner runs on every scheduled job, synchronously. Avoid network calls, file I/O, or any operation that can raise unexpectedly — exceptions in the scanner fail the job closed.
A job can complete successfully but fail to reach the delivery target. Check both fields:
result = executor.run_job(job)
if result.status == "success" and result.delivery_error:
    print(f"Job ran OK but delivery failed: {result.delivery_error}")
    print(f"Full output saved at: {result.audit_path}")

Schedule agents with cost limits and retries

Command-line scheduling