Core Concepts
Triggers

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 capture

Internal 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 schedules

The 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. @schedule accepts a timezone= (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 prod

On 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.paid event fires the trigger; the agent generates a personalized onboarding email and triggers your email service.
  • Slack support — a Slack workflow forwards #support messages 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) — changing name deletes 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

HowBest forInput sourceFrequency
APIUser actions, ad-hoc jobs, internal toolsYour backendOn-demand
ScheduleMonitoring, reports, periodic processingConfigured onceCron-based
TriggerExternal events (GitHub, Stripe, Slack)HTTP request bodyEvent-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.