> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chronicle-labs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Events

> Send events, traces, and feedback into Chronicle, then query the history.

Chronicle records what agents and systems do as **events**. Send them from your
own code, from your agent's tool calls, or from an OpenTelemetry exporter, then
query them in Timeline or through the SDK. Real activity is where you find the
cases worth turning into tasks.

Use a [configured SDK client](/api-reference/languages/overview#use-the-guide-examples).
The examples share that client and the IDs returned by earlier steps.

## Methods

| Method                                    | HTTP                           | Does                                           |
| ----------------------------------------- | ------------------------------ | ---------------------------------------------- |
| `events.create(...)`                      | `POST /v1/events`              | Record one event.                              |
| `events.create_batch([...])`              | `POST /v1/events/batch`        | Record up to 1,000 events.                     |
| `events.list(...)`                        | `GET /v1/events`               | Query events by source, type, entity, or time. |
| `events.timeline(entity_type, entity_id)` | `GET /v1/timeline/{type}/{id}` | Everything about one entity, across sources.   |
| `events.search(query)`                    | `POST /v1/search`              | Semantic search over event content.            |
| `events.stream(...)`                      | `GET /v1/events/stream`        | Iterate new events as they arrive.             |
| `traces.create(...)`                      | `POST /v1/traces/track`        | Record a trace with spans.                     |
| `signals.create(...)`                     | `POST /v1/signals/track`       | Attach feedback to an event.                   |

Writes need `events:write`, `traces:write`, or `signals:write`; reads need
`events:read`. See [Authentication](/api-reference/authentication).

## Record an event

An event is a fact: where it came from (`source`), what category (`topic`),
what happened (`event_type`), which records it's about (`entities`), and the
details (`payload`).

<CodeGroup dropdown>
  ```python Python theme={null}
  created_event = client.events.create(
      source="invoice-agent",
      topic="invoice-triage",
      event_type="issue.triaged",
      entities={"issue": "ENG-2", "customer": "northwind"},
      payload={"issue": "ENG-2", "status": "in_progress", "handoff": "Assigned for follow-up"},
  )
  print(created_event.id)
  ```

  ```typescript TypeScript theme={null}
  const event = await client.events.create({
    source: "invoice-agent",
    topic: "invoice-triage",
    eventType: "issue.triaged",
    entities: { issue: "ENG-2", customer: "northwind" },
    payload: { issue: "ENG-2", status: "in_progress", handoff: "Assigned for follow-up" },
  });
  console.log(event.id);
  ```

  ```javascript JavaScript theme={null}
  const event = await client.events.create({
    source: "invoice-agent",
    topic: "invoice-triage",
    eventType: "issue.triaged",
    entities: { issue: "ENG-2", customer: "northwind" },
    payload: { issue: "ENG-2", status: "in_progress", handoff: "Assigned for follow-up" },
  });
  console.log(event.id);
  ```

  ```go Go theme={null}
  event, err := client.Events.Create(ctx, chronicle.EventCreateParams{
      Source:    "invoice-agent",
      Topic:     "invoice-triage",
      EventType: "issue.triaged",
      Entities:  map[string]string{"issue": "ENG-2", "customer": "northwind"},
      Payload:   map[string]any{"issue": "ENG-2", "status": "in_progress", "handoff": "Assigned for follow-up"},
  })
  if err != nil {
      return err
  }
  fmt.Println(event.ID)
  ```

  ```rust Rust theme={null}
  let event = client.events().create(EventCreateParams {
      source: "invoice-agent".into(),
      topic: "invoice-triage".into(),
      event_type: "issue.triaged".into(),
      entities: [("issue".into(), "ENG-2".into()), ("customer".into(), "northwind".into())].into(),
      payload: serde_json::json!({ "issue": "ENG-2", "status": "in_progress", "handoff": "Assigned for follow-up" }),
      ..Default::default()
  }).await?;
  println!("{}", event.id);
  ```
</CodeGroup>

`source`, `topic`, and `event_type` are required. `timestamp` defaults to now.
Use stable identifiers in `entities` (the issue key, the order number) so the
same record's history lines up across sources. `events.create_batch` takes a
list of the same shape.

Recording an event stores telemetry. It doesn't change anything in a twin or
prove that a change happened; twins record their own mutations.

## Query events

<CodeGroup dropdown>
  ```python Python theme={null}
  page = client.events.list(source="invoice-agent", entity_type="issue", entity_id="ENG-2", limit=50)
  for item in page.auto_paging_iter():
      print(item.event_time, item.event_type, item.payload)

  history = client.events.timeline("issue", "ENG-2")
  for item in history.items:
      print(item.source, item.event_type)
  ```

  ```typescript TypeScript theme={null}
  const page = await client.events.list({ source: "invoice-agent", entityType: "issue", entityId: "ENG-2", limit: 50 });
  for await (const event of page) console.log(event.eventTime, event.eventType, event.payload);

  const history = await client.events.timeline("issue", "ENG-2");
  for (const event of history.items) console.log(event.source, event.eventType);
  ```

  ```javascript JavaScript theme={null}
  const page = await client.events.list({ source: "invoice-agent", entityType: "issue", entityId: "ENG-2", limit: 50 });
  for await (const event of page) console.log(event.eventTime, event.eventType, event.payload);

  const history = await client.events.timeline("issue", "ENG-2");
  for (const event of history.items) console.log(event.source, event.eventType);
  ```

  ```go Go theme={null}
  page, err := client.Events.List(ctx, chronicle.EventListParams{
      Source: "invoice-agent", EntityType: "issue", EntityID: "ENG-2", Limit: 50,
  })
  if err != nil {
      return err
  }
  for {
      for _, event := range page.Items { fmt.Println(event.EventTime, event.EventType, event.Payload) }
      if !page.HasNextPage() { break }
      page, err = page.GetNextPage(ctx)
      if err != nil { return err }
  }

  history, err := client.Events.Timeline(ctx, "issue", "ENG-2")
  if err != nil {
      return err
  }
  for _, event := range history.Items {
      fmt.Println(event.Source, event.EventType)
  }
  ```

  ```rust Rust theme={null}
  let mut page = client.events().list(EventListParams {
      source: Some("invoice-agent".into()),
      entity_type: Some("issue".into()),
      entity_id: Some("ENG-2".into()),
      limit: Some(50),
      ..Default::default()
  }).await?;
  loop {
      for event in &page.items { println!("{} {} {:?}", event.event_time, event.event_type, event.payload); }
      if !page.has_next_page() { break; }
      page = page.get_next_page().await?;
  }

  let history = client.events().timeline("issue", "ENG-2").await?;
  for event in &history.items {
      println!("{} {}", event.source, event.event_type);
  }
  ```
</CodeGroup>

| Filter                          | Meaning                           |
| ------------------------------- | --------------------------------- |
| `source`, `topic`, `event_type` | Match event metadata.             |
| `entity_type` + `entity_id`     | Match a linked record. Pass both. |
| `since`                         | A window like `last_7d`.          |
| `limit`                         | Page size, up to 200.             |

The filtered query iterates every page. The `history` example displays the
first Timeline page; use the same pagination helpers for its full history.
[Explore the linked records](/product-explorer#connected-records) to see why
stable entity IDs connect activity across services. Results are newest first. `events.search(query="duplicate invoice")` does a
semantic search when your deployment has embeddings enabled.

<Frame caption="A captured Linear issue update shows the exact fields that changed. This is twin request activity; custom events and traces remain separate records." className="product-capture">
  <img src="https://mintcdn.com/chroniclelabs-0d363efc/BRuFTKj-kt84RfoY/images/product/worldsmith-mutation-focused.png?fit=max&auto=format&n=BRuFTKj-kt84RfoY&q=85&s=be54f02ba67c2f30936daa83eb63d16c" alt="State-change drawer for linear.issues with before and after values for state_id, started_at, and updated_at" style={{maxWidth: "540px", margin: "0 auto"}} width="539" height="857" data-path="images/product/worldsmith-mutation-focused.png" />
</Frame>

[Open the state-change image](/images/product/worldsmith-mutation-focused.png).

[Read the matching request](/api-reference/twins#read-what-happened), then use
trace IDs and entity references to correlate your own events with the run.

## Record a trace

A trace groups the steps of one agent invocation. Give spans clear names and
put the IDs your team searches by in `attributes`.

<CodeGroup dropdown>
  ```python Python theme={null}
  client.traces.create(
      trace_id="invoice-triage-2026-09-10-001",
      name="Invoice triage",
      attributes={"customer": "northwind"},
      spans=[
          {"span_id": "s1", "name": "linear.issue.update", "kind": "tool", "status": "ok",
           "duration_ms": 120, "attributes": {"issue": "ENG-2", "target_status": "in_progress"}},
          {"span_id": "s2", "name": "slack.chat.postMessage", "kind": "tool", "status": "ok",
           "duration_ms": 80, "attributes": {"channel": "erp-alerts"}},
      ],
  )
  ```

  ```typescript TypeScript theme={null}
  await client.traces.create({
    traceId: "invoice-triage-2026-09-10-001",
    name: "Invoice triage",
    attributes: { customer: "northwind" },
    spans: [
      { spanId: "s1", name: "linear.issue.update", kind: "tool", status: "ok",
        durationMs: 120, attributes: { issue: "ENG-2", targetStatus: "in_progress" } },
      { spanId: "s2", name: "slack.chat.postMessage", kind: "tool", status: "ok",
        durationMs: 80, attributes: { channel: "erp-alerts" } },
    ],
  });
  ```

  ```javascript JavaScript theme={null}
  await client.traces.create({
    traceId: "invoice-triage-2026-09-10-001",
    name: "Invoice triage",
    attributes: { customer: "northwind" },
    spans: [
      { spanId: "s1", name: "linear.issue.update", kind: "tool", status: "ok",
        durationMs: 120, attributes: { issue: "ENG-2", targetStatus: "in_progress" } },
      { spanId: "s2", name: "slack.chat.postMessage", kind: "tool", status: "ok",
        durationMs: 80, attributes: { channel: "erp-alerts" } },
    ],
  });
  ```

  ```go Go theme={null}
  _, err = client.Traces.Create(ctx, chronicle.TraceCreateParams{
      TraceID:    "invoice-triage-2026-09-10-001",
      Name:       "Invoice triage",
      Attributes: map[string]any{"customer": "northwind"},
      Spans: []chronicle.Span{
          {SpanID: "s1", Name: "linear.issue.update", Kind: "tool", Status: "ok",
              DurationMs: 120, Attributes: map[string]any{"issue": "ENG-2", "target_status": "in_progress"}},
          {SpanID: "s2", Name: "slack.chat.postMessage", Kind: "tool", Status: "ok",
              DurationMs: 80, Attributes: map[string]any{"channel": "erp-alerts"}},
      },
  })
  if err != nil { return err }
  ```

  ```rust Rust theme={null}
  client.traces().create(TraceCreateParams {
      trace_id: "invoice-triage-2026-09-10-001".into(),
      name: "Invoice triage".into(),
      attributes: serde_json::json!({ "customer": "northwind" }),
      spans: vec![
          Span { span_id: "s1".into(), name: "linear.issue.update".into(), kind: Some("tool".into()), status: Some("ok".into()),
              duration_ms: Some(120), attributes: serde_json::json!({ "issue": "ENG-2", "target_status": "in_progress" }), ..Default::default() },
          Span { span_id: "s2".into(), name: "slack.chat.postMessage".into(), kind: Some("tool".into()), status: Some("ok".into()),
              duration_ms: Some(80), attributes: serde_json::json!({ "channel": "erp-alerts" }), ..Default::default() },
      ],
  }).await?;
  ```
</CodeGroup>

Each span needs a `span_id` and `name`; `parent_span_id`, `started_at`,
`ended_at`, `duration_ms`, `kind`, `status`, and `attributes` are optional. Up
to 1,000 spans per call.

### OpenTelemetry

If you already export OTLP traces, point the exporter at Chronicle instead of
sending spans by hand. Set the exporter's endpoint to your Chronicle URL plus
`/v1/traces`, add the header `Authorization: Bearer <key with traces:write>`,
and keep protobuf or JSON encoding. Chronicle stores the spans alongside events
recorded through the SDK.

## Attach feedback

A signal is feedback about an event: a reviewer's verdict, a thumbs-up, a
label. It uses the `id` returned when the event was created.

<CodeGroup dropdown>
  ```python Python theme={null}
  client.signals.create(
      event_id=created_event.id,
      signal_name="reviewed",
      properties={"review": "Handoff has an actionable next step"},
  )
  ```

  ```typescript TypeScript theme={null}
  await client.signals.create({
    eventId: event.id,
    signalName: "reviewed",
    properties: { review: "Handoff has an actionable next step" },
  });
  ```

  ```javascript JavaScript theme={null}
  await client.signals.create({
    eventId: event.id,
    signalName: "reviewed",
    properties: { review: "Handoff has an actionable next step" },
  });
  ```

  ```go Go theme={null}
  _, err = client.Signals.Create(ctx, chronicle.SignalCreateParams{
      EventID:    event.ID,
      SignalName: "reviewed",
      Properties: map[string]any{"review": "Handoff has an actionable next step"},
  })
  if err != nil { return err }
  ```

  ```rust Rust theme={null}
  client.signals().create(SignalCreateParams {
      event_id: event.id.clone(),
      signal_name: "reviewed".into(),
      properties: serde_json::json!({ "review": "Handoff has an actionable next step" }),
      ..Default::default()
  }).await?;
  ```
</CodeGroup>

Signals are observations. Pass/fail in an evaluation comes from the task's
scorers, not from signals.

## Follow events live

`events.stream` yields events as they arrive, with the same filters as
`events.list`. It reconnects on its own and resumes where it left off.

<CodeGroup dropdown>
  ```python Python theme={null}
  for event in client.events.stream(source="invoice-agent"):
      print(event.event_type, event.entities)
  ```

  ```typescript TypeScript theme={null}
  for await (const event of client.events.stream({ source: "invoice-agent" })) {
    console.log(event.eventType, event.entities);
  }
  ```

  ```javascript JavaScript theme={null}
  for await (const event of client.events.stream({ source: "invoice-agent" })) {
    console.log(event.eventType, event.entities);
  }
  ```

  ```go Go theme={null}
  stream, err := client.Events.Stream(ctx, chronicle.EventStreamParams{Source: "invoice-agent"})
  if err != nil {
      return err
  }
  defer stream.Close()
  for stream.Next() {
      event := stream.Event()
      fmt.Println(event.EventType, event.Entities)
  }
  if err := stream.Err(); err != nil { return err }
  ```

  ```rust Rust theme={null}
  let mut stream = client.events().stream(EventStreamParams {
      source: Some("invoice-agent".into()),
      ..Default::default()
  }).await?;
  while let Some(event) = stream.next().await {
      let event = event?;
      println!("{} {:?}", event.event_type, event.entities);
  }
  ```
</CodeGroup>

If replay exceeds 1,000 matching events, the stream can report a replay-limit
error. Catch up with `events.list` from your last stored time window before
resuming. Deduplicate on `event.id`; do not treat a disconnected stream as proof
that no events occurred.

## What lives where

* **Events** you send: `events.list`, Timeline.
* **Twin requests and mutations**: [`twins.activity`](/api-reference/twins#read-what-happened).
* **What an agent did in a trial**: [`evaluations.trials.retrieve`](/api-reference/evaluations#inspect-trials).
