Agent
An agent is the deployable unit: your function plus a name, a version, an optional schedule, triggers, and a budget. @agent(name=...) registers a function as one, so the CLI can discover it and the hosted worker pool can load it once and run it.
Papayya does not ship LLM provider adapters and does not call LLMs on your behalf. You bring your own LLM SDK (OpenAI, Anthropic, etc.) and call it directly inside your function. Papayya wraps the function with durable execution, budget enforcement, and observability.
Defining an agent
An @agent-decorated function receives a durable run handle as its first argument. Call run.step(...) to checkpoint a side effect and run.complete(...) to finish the item; the rest of your signature is yours to shape.
from papayya import agent
# TODO(verify): budget_usd kwarg not seen in examples; confirm against papayya-python SDK
@agent(name="research-bot", budget_usd=1.00)
def research_bot(run, prompt: str) -> str:
"""Your agent loop — call your LLM SDK directly."""
from openai import OpenAI
client = OpenAI()
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a research assistant."},
{"role": "user", "content": prompt},
],
)
answer = resp.choices[0].message.content
run.complete(answer)
return answer
# Run locally — just call the function
if __name__ == "__main__":
print(research_bot("What are the latest AI trends?"))@agent requires an explicit name, used as the deploy slug.
The
runhandle. For quality capture without touching your business logic, mark the leaf that calls your model with@papayya.llm(see below) — Papayya records the step ambiently against the item the decorator opened. For explicit checkpointed steps, callrun.step(...)on the injected handle, or open a standalonepapayya().item(...)handle from the SDK overview.
Recording model calls
Inside the body, decorate the function that actually calls your provider with @papayya.llm. Each call becomes an LLM step (model, tokens, timing) and its return value is outcome-inspected — a refusal or empty result flips the item to degraded with no check written anywhere.
import papayya
from papayya import agent
from openai import OpenAI
@papayya.llm
def ask(messages: list[dict]) -> dict:
client = OpenAI()
return client.chat.completions.create(model="gpt-4o-mini", messages=messages).to_dict()
@agent(name="research-bot", budget_usd=1.00)
def research_bot(run, prompt: str) -> str:
out = ask([{"role": "user", "content": prompt}])
return out["choices"][0]["message"]["content"]Multiple agents per file
@agent(name="researcher", budget_usd=1.00)
def researcher(run, prompt: str):
...
@agent(name="summarizer", budget_usd=0.50)
def summarizer(run, text: str):
...The CLI discovers every @agent function in the file automatically. The worker uses PAPAYYA_AGENT_FUNCTION to select which one to run.
Schedules and triggers
Attach a cron schedule or an inbound trigger by stacking @papayya.schedule / @papayya.trigger above the agent decorator (they read the registration the inner decorator produces — wrong order raises DecoratorTargetError).
import papayya
@papayya.schedule("0 9 * * 1-5", timezone="UTC") # weekdays 9am — each fire is one run
@papayya.trigger(name="inbound-ticket", secret_env="TICKET_HMAC_SECRET")
@agent(name="triage-ticket", budget_usd=0.25)
def triage_ticket(run, ticket: dict) -> str:
...@papayya.schedule(cron, *, timezone="UTC")— cron and timezone are validated at decoration time. Stack multiple to fire on several crons.@papayya.trigger(*, name, secret_env)—namematches^[a-zA-Z0-9_-]{1,64}$;secret_envnames the process env var holding the HMAC shared secret. The trigger URL is server-assigned at create time. (The inbound-trigger transport is a webhook under the hood;triggeris the product noun.)
Schedules and triggers reconcile on the next papayya deploy — see Schedules & Triggers.
Deploying
# Zero-arg — discovers agent.py in cwd
papayya deploy
# Explicit file
papayya deploy agents.pyTriggering runs
From the CLI:
papayya run research-bot "AI trends" # trigger one run, stream it
papayya runs submit --agent research-bot --file items.jsonl # a whole hosted pile
papayya status <id>
papayya logs <id>Or programmatically, through the hosted client:
from papayya import Papayya
client = Papayya(api_key="cpk_...")
# Submit one invocation (a run over N items) for an agent.
run = client.runs.create(agent_id="ag_...", items=["AI trends"], budget_cents=100)
# Read a per-item item back / stream its steps.
item_id = run["items"][0]["id"]
detail = client.items.get(item_id)
steps = client.items.steps(item_id)
for event in client.items.stream(item_id):
...Decorator parameters
Passed to @agent(...).
| Parameter | Type | Required | Description |
|---|---|---|---|
name | str | Yes | Agent identifier / deploy slug. |
budget_usd | float | No | Per-run spend cap in USD. Enforced by the hosted worker; see Budget. |
model | str | No | Display label only (e.g. gpt-4o-mini). Never used to route to a provider, select an SDK, or compute cost — your code picks the provider. |
instructions | str | No | System prompt — stored as agent config metadata only. Your code passes it to the LLM; Papayya does not inject it into your provider call. |
max_steps | int | No | Maximum LLM calls per run (default: 50). |
tools | list | No | Tool metadata for dashboard display only — your code handles actual tool execution. See @tool below. |
max_duration_seconds | float | No | Wall-clock ceiling for one item, up to 86400. None means the server's default of 1800. A per-run timeout_seconds outranks it. Signal-based watchdog (Unix); cannot interrupt blocking C calls, so pair it with explicit socket timeouts. See Long-running agents. |
agent_version | str | No | Opaque version string stamped on every run + step; the replay-mismatch gate. If omitted, resolved from PAPAYYA_AGENT_VERSION, then git rev-parse --short HEAD, then "unknown". |
concurrency_per_key | int | No | Cap on concurrent in-flight items per partition_key value. Over the cap, items wait in the queue until one finishes. None disables. See Per-tenant caps. |
rate_limit | str | No | Cap on how many of one partition_key value's items start per window. Format "N/min" or "N/sec"; validated at decoration time. None disables. |
Agent version resolution
Papayya logs the resolved version at registration time so you can spot a misconfigured CI build before it surfaces at replay:
INFO papayya.agent: registered 'enrich' v=abc1234 (source=git)Sources are decorator | env | git | unknown. If the source is unknown, the line is emitted at WARNING — there was no decorator argument, no PAPAYYA_AGENT_VERSION, and git rev-parse --short HEAD failed. Pin the version explicitly via the decorator arg or set the env var in your deploy pipeline.
Tool definitions
@tool turns a typed function into a schema-only ToolDefinition for the dashboard and for passing to your provider — Papayya infers a JSON-Schema parameters object from the signature. It does not execute the tool for you; your code owns tool-call dispatch.
from papayya import tool
@tool
def get_weather(city: str) -> dict:
"""Look up current weather for a city."""
...Registry functions
| Function | Description |
|---|---|
get_registry() | Returns the registry of all registered agents, keyed by (name, agent_version). |
get_agent(name, version=None) | Look up a registration by name (latest-registered wins when version is omitted). |
Why BYOF?
Papayya is infrastructure. Shipping first-class provider adapters would mean tracking every SDK update, pricing change, and tool-call quirk from every provider — and when one breaks, it breaks your runs mid-flight. Instead you call the provider SDK directly, pinned to whichever version you control, and Papayya focuses on durability, checkpointing, and observability.