Core Concepts
Multi-tenancy

Multi-tenancy

Most production workloads serve more than one customer. Papayya's partition_key convention makes the tenant a first-class slice of the run → item ledger, so the dashboard, queries, cost rollups, and slice replay can read it without you wiring any custom infrastructure.

Tenant is the taught face of partition_key. You declare it once, at the loop boundary, as a callable over each element:

partition_key=lambda t: t["tenant"]

The mechanism is general — partition_key can identify a tenant, a customer, an end-user, or a region — but tenancy is the dominant use case, so that's how we teach it.

Papayya makes tenant a declared dimension of your own execution — a key it items and lets you slice by. It does not operate your tenancy: no isolation boundaries, no per-tenant quotas, no provisioning. You bring the key; Papayya makes it first-class in the ledger.

Declare the tenant at the call site

Pass partition_key= (and item_id=) when you invoke your @agent. Papayya consumes both and does not forward them to your function, so nothing in your signature changes:

import papayya
from papayya import agent
 
@agent(name="triage-ticket", model="claude-sonnet-5")
def triage(run, ticket: dict) -> str:
    ...
 
for ticket in TICKETS:
    print(triage(
        ticket,
        item_id=ticket["id"],            # your identity for this item
        partition_key=ticket["tenant"],  # whose item it is — the tenant
    ))

You declare the tenant where the work starts — not threaded through any function signature. If you want to read it, declare partition_key in your own signature and it is passed through as well.

The two cloud paths take the same two values:

papayya run triage-ticket '{"id": "t-9", "tenant": "acme"}' \
    --item-id t-9 --partition-key acme

and the dashboard's Run now dialog has an Item ID and a Tenant field.

Where the key lands

Papayya writes the value into the indexed partition_key column on both the run and the item rows it mints. That single indexed column is what powers every per-tenant view downstream — no schema changes on your side.

What a missing key costs

partition_key is optional, and omitting it is not an error — the column is left NULL. But a NULL partition key is invisible to every per-tenant slice: Explore's Partition column renders a dash, and "which of my customers is broken?" has no answer for that item. Declare it on anything that belongs to a customer.

What this unlocks

Because the tenant is a first-class column on runs and items, you get per-tenant lenses on your own execution for free:

  • Per-tenant filters in the dashboard — list runs and items for a specific tenant.
  • Per-tenant outcome — see the degraded_count and worst_outcome_status for one tenant's slice of a run.
  • Cost rollups — aggregate per-item cost by tenant for invoicing or chargeback.
  • Slice replay — recover just one tenant's not-ok items: papayya replay --run <id> --tenant acme.

These are all views and slices over execution Papayya already owns — not tenancy machinery Papayya runs on your behalf.

Schema

The SDK persists the key locally (offline store) and in the cloud (control-pane migration). Both stores expose:

ColumnTypeNotes
runs.partition_keyTEXT, indexedValue returned by partition_key(element) for the run.
items.partition_keyTEXT, indexedSame key denormalized onto each item for per-tenant queries.

Rows written without a partition_key in scope have NULL in both columns and are unaffected — the convention is opt-in per loop.