---
title: Runs and events
description: The core data model. A run is one invocation of an agent; events are the observations inside it.
section: concepts
order: 4
---

# Runs and events

Two records: a **run** and the **events** inside it.

## Run

One execution of an agent.

| Field | Notes |
|-------|-------|
| `id` | Client-generated, `run_<region>_...`. See [Identifiers](/docs/concepts/identifiers). |
| `agent` | Slug. |
| `started_at` / `received_at` | Client clock / server clock. |
| `status` | `running`, `success`, `failed`, `timeout`, `cancelled`. |
| `finished_at` | Set on close. |
| `customer_id`, `feature` | Optional attribution tags. |
| `parent_run_id` | Optional, for cross-agent traces. |

Runs are immutable once finished. Re-finishing a terminal run returns the existing record idempotently.

## Event

A discrete observation inside a run. SDKs emit events automatically; you can append your own.

| Type | Source |
|------|--------|
| `llm_call` | Auto-captured for Anthropic and OpenAI; report any other provider with `run.event("llm_call", {...})` |
| `tool_call` | Auto-captured for Anthropic tool use; report your own for anything else |
| `log` | `run.log("message")` |
| `decision` | Explicit branching points |
| `score` | `run.score(name, value)`, self-reported quality |

Each event has a client-generated `id` (`evt_<region>_...`), timestamp, type, and JSON `data` payload.

## Lifecycle

```
run start  ─►  event  ─►  event  ─►  event  ─►  run finish
   │                                                │
   └────── server stamps received_at ───────────────┘
```

The SDK constructs `run.id` locally before any network call. Log it, pass it to child processes, include it in your own errors immediately. Events can be sent in any order.

:::codetabs
```python
with agentping.run("support-triage", customer_id="acme-corp") as run:
    run.log("classifying ticket")
    decision = classify(ticket)
    run.score("confidence", decision.confidence)
    return decision
```
```typescript
const run = agentping.run("support-triage", { customerId: "acme-corp" });
run.log("classifying ticket");
const decision = await classify(ticket);
run.score("confidence", decision.confidence);
await run.finish({ status: "success" });
```
```go
run := agentping.Run("support-triage",
    agentping.WithCustomerID("acme-corp"),
)
run.Log("classifying ticket")
decision := classify(ticket)
run.Score("confidence", decision.Confidence)
run.Finish(agentping.RunStatusSuccess)
```
:::

## Parent runs

When agent A spawns agent B, set the parent so traces link across processes:

- `AGENTPING_PARENT_RUN=run_eu_018f3a2b9c1d7e8fa4b9c2d7e8f1a3b6` (env var, read on init)
- `parent_run_id` parameter on the child run

A failure on a child surfaces the root run too: a five-step pipeline that breaks at step four shows up as one incident, not four.

## Heartbeats

A heartbeat is a run with `started_at == finished_at` and no events. Same table, same dashboard, same alerts. See [Heartbeats](/docs/pulse/heartbeats).

:::note
Every record stores `started_at` (client) and `received_at` (server). The dashboard sorts on `started_at` unless clock skew is detected (>5 min ahead, or >24h behind `received_at`), then it falls back to `received_at` with a drift indicator.
:::
