SDK Reference
signal()

papayya.signal()

Tell Papayya that the world judged one of your items — a thumbs-down, a refund, a support ticket, a human editing your agent's output.

import papayya
 
def on_thumbs_down(order):
    papayya.signal(order.id, agent="triage")

It is keyed on your id, because that is the only id the handler holds. Papayya resolves it to whichever item answered for that order. See Recovery for what you then do with the flags, and the API for the wire contract.

Signature

papayya.signal(
    item_id,                  # your id for the item, as declared on submit
    *,
    agent,                    # the agent that produced it
    verdict="bad",            # good | bad | corrected
    source="thumbs",          # thumbs | ticket | edit | refund | rejection | dashboard
    reason=None,              # free text — what the person knew
    external_id=None,         # your id for the SIGNAL (a webhook event id)
    occurred_at=None,         # when the world judged; defaults to now
    partition_key=None,       # required if you submitted under one
    api_key=None, base_url=None,
)

The defaults describe a thumbs-down, which is the common case, so the minimal call is one line.

What it will and will not do to your handler

It never blocks. The request is dispatched to a background thread and the call returns immediately. One attempt, a 0.75-second timeout, no retries — a signal is evidence, not a transaction, and a lost one is better than a hung checkout. (For comparison: the same call through Papayya's ordinary API client, which retries because a run submission should, blocks for 1.5 seconds against a dead port and much longer against a host that accepts and never answers.)

A delivery failure is a log line, never an exception. If the control plane is unreachable, your handler still returns.

Two things raise, on your thread, before anything is sent:

  • no API key — PapayyaAPIError. A deployment that signals into the void would otherwise look identical to one that works, forever.
  • a verdict or source outside the allowed set — ValueError.

A rejected signal is logged at ERROR. An agent slug that does not exist, for instance. It is your bug and it will fail the same way every time, but there is nobody to raise it to from a background thread — so watch the papayya.signal logger in your app.

It returns a concurrent.futures.Future. Ignore it in production; call .result() in a test when you need the send to have finished.

Duplicates are yours to decide

Pass external_id — your id for the signal, like a webhook event id — and a retried webhook stores one row. Without it, two calls store two rows, on purpose: an item accumulates signals, and two people complaining about one order is two pieces of evidence, not a duplicate.

Do not put the order id in external_id. A second, genuine complaint about that order would be taken for the first one arriving twice, and silently dropped.

Which item it names

The most recent one that had already produced output when the world judged. If your order was re-driven — one of your ids legitimately maps to several of our items over time — the complaint lands on the item that actually answered by then, not on the first attempt.

That is decided every time someone asks, not once when you post. A signal that arrives before we have finished recording the item is stored anyway and starts counting the moment it can.

papayya.signal(
    order.id,
    agent="triage",
    partition_key=order.tenant,   # not guessed — see below
    verdict="corrected",
    source="edit",
    reason="agent named the wrong policy",
    external_id=event.id,
)

If you submitted under a partition_key, pass it here. A signal without one names only items submitted without one: Papayya will not pick between your tenants, and the API response tells you when your order id exists under partitions you did not name.