Triggers
Every run has to start somehow. Papayya gives you three ways to start one, covering every production pattern: call the API on demand, run on a schedule, or expose a trigger that external systems fire over HTTP.
Schedules and triggers are wired in code — decorators stacked above your @agent function — and reconciled by papayya deploy. You can also inspect and manage them imperatively — papayya schedules … and papayya triggers … — but the decorators stay the source of truth the deploy reconciles against. See Schedules & Triggers for the full mechanics.
A reminder on the nouns: one invocation of an agent is a run, and a run processes items. However you start it, you get a run.
API (on-demand)
The most common way to start a run. Your backend calls the Papayya API when a user action or internal event needs one.
When to use: User-initiated workflows, ad-hoc jobs, anything where your code decides when to start a run.
Real-world examples
SaaS product with AI features — a user clicks "Analyze competitors" in your app:
# Your backend handler
import requests
def handle_analysis_request(query):
resp = requests.post(
"https://api.getpapayya.com/v1/agents/research-agent/runs",
headers={"X-Api-Key": "cpk_..."},
json={
"agent": "research-agent",
"input": query,
"budget_usd": 1.00,
"callback_url": "https://api.yourapp.com/runs/complete",
},
)
run = resp.json()
# Return immediately — Papayya runs the agent in the background
return {"run_id": run["id"], "status": "processing"}Batch pipeline — enrich 500 leads overnight. That's one run over 500 items, not 500 runs: hand the hosted worker pool a JSONL where each line is one item.
papayya runs submit --agent lead-enrichment --file leads.jsonl
# each line in leads.jsonl becomes one item; Papayya owns concurrency, budgets, and outcome captureInternal tools — start a run locally by running your agent script:
python agent.py "Review PR #342 in papayya"…or over HTTP from any script:
curl -X POST https://api.getpapayya.com/v1/agents/code-reviewer/runs \
-H "X-Api-Key: cpk_..." \
-d '{
"agent": "code-reviewer",
"input": "Review PR #342 in papayya",
"budget_usd": 2.00
}'Callback URL
Pass a callback_url when you start a run and Papayya POSTs the result when it finishes. This eliminates polling — fire and forget.
{
"agent": "...",
"input": "...",
"callback_url": "https://your-api.com/run-complete"
}The callback payload carries the run id, status, the run's rolled-up outcome (worst_outcome_status and degraded_count), token counts, cost, and error details. The run's input and output are not in the payload — fetch GET /v1/durable/runs/{runId} if you need them. Retried up to 3 times with exponential backoff. See the API reference for the full schema.
Schedules (recurring)
Cron-based starts for agents that run on a regular cadence. Each cron fire is one run.
When to use: Monitoring, reporting, periodic data processing — anything that runs on a clock.
Schedules are declared with @schedule decorators stacked above the agent, and reconciled by papayya deploy:
from papayya import agent, schedule
@schedule(cron="0 9 * * 1-5") # weekdays at 9am UTC — each fire is one run
@agent(name="competitor-monitor", budget_usd=2.00)
def competitor_monitor(run, payload):
# Your loop. The scheduled fire passes a fixed input;
# if you need dynamic per-run input, use the API on-demand path above.
...papayya deploy --env dev # bundles code + reconciles schedulesThe agent's input and budget are owned by the @agent decorator — schedules only control when the run fires. Stack more @schedule decorators to add cadences; each fire is one run.
You can list and toggle schedules imperatively — papayya schedules list, papayya schedules disable <id>, papayya schedules enable <id> — but the decorators stay the declarative source of truth.
v1 limits
- Times default to UTC.
@scheduleaccepts atimezone=(IANA name); see Schedules & Triggers for the caveat. - One input per agent. Every scheduled run of an agent shares whatever input you wire in code.
See Schedules & Triggers for reconciliation rules and rename-vs-delete semantics.
Triggers (event-driven)
A trigger lets external systems start a run over HTTP. Each trigger gets a stable URL — a webhook endpoint, in HTTP terms — and a server-generated HMAC bearer token. When an external system POSTs to that URL, the trigger fires and starts one run.
When to use: Reacting to external events — GitHub pushes, Stripe payments, Slack messages, form submissions.
Triggers are declared with @trigger decorators above each agent:
from papayya import agent, trigger
@trigger(name="github-push", secret_env="GITHUB_WEBHOOK_SECRET")
@agent(name="pr-reviewer")
def pr_reviewer(run, payload):
...
@trigger(name="stripe-invoice", secret_env="STRIPE_WEBHOOK_SECRET")
@agent(name="invoice-processor")
def invoice_processor(run, payload):
...
@trigger(name="slack-support", secret_env="SLACK_WEBHOOK_SECRET")
@agent(name="support-agent")
def support_agent(run, payload):
...papayya deploy --env prodOn first reconciliation the CLI prints the trigger's URL and the plaintext secret once — store it in the env var named by secret_env so the sending system can sign requests.
✓ trigger created pr-reviewer/github-push
URL: https://api.getpapayya.com/v1/webhooks/whk_abc123/trigger
Secret: whk_secret_a8f3... (only shown now — store as $GITHUB_WEBHOOK_SECRET)The request body becomes the run's input — any valid JSON or plain text. You can pass a callback URL via header:
curl -X POST https://api.getpapayya.com/v1/webhooks/whk_abc123/trigger \
-H "Authorization: Bearer $GITHUB_WEBHOOK_SECRET" \
-H "X-Callback-URL: https://your-api.com/done" \
-d '{"event": "push", "repo": "papayya", "branch": "main"}'You can also manage triggers imperatively: papayya triggers create, papayya triggers list, papayya triggers delete.
Real-world patterns
- GitHub PR review — the trigger receives the push payload; the agent fetches the diff, analyzes the code, posts review comments.
- Stripe payment processing — an
invoice.paidevent fires the trigger; the agent generates a personalized onboarding email and triggers your email service. - Slack support — a Slack workflow forwards
#supportmessages to the trigger; the agent researches the issue and drafts a reply.
v1 limits
- HMAC shared secret only. OAuth and per-event signature schemes are deferred.
- No request transformation. The raw body is the run's input — parse it inside your agent function.
- Rename rotates the URL. Trigger identity is
(agent, name)— changingnamedeletes the old trigger and creates a new one with a new URL. The CLI warns loudly when this happens.
Choosing how to start a run
| How | Best for | Input source | Frequency |
|---|---|---|---|
| API | User actions, ad-hoc jobs, internal tools | Your backend | On-demand |
| Schedule | Monitoring, reports, periodic processing | Configured once | Cron-based |
| Trigger | External events (GitHub, Stripe, Slack) | HTTP request body | Event-driven |
All three share the same execution guarantees: crash recovery, budget enforcement, step-level tracing, per-item outcome capture, and dashboard visibility. Every run — whether started by an API call, a cron schedule, or a GitHub push — goes through the same pipeline.
You can combine them for the same agent. A competitor-monitoring agent might run on a daily schedule and be startable via the API for ad-hoc checks — stack the @schedule decorator and call the API from your backend.