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

# Systems, objects and actions

> CRM, ERP and help desk events, business objects, and the actions that close open items.

Niadra is the memory of every agent in the company: the ones that serve customers and the ones that work inside, in the CRM, the ERP, the help desk, billing and orders. For that, the memory takes in three things besides conversations: the **system events** your systems already emit, the **business objects** they refer to and the **actions** agents take in those systems. That is how the voice agent knows at 2:07 pm what the billing agent did at 2:06 pm.

## System events

A system event is a state change in a system of record: order created, invoice disputed, ticket reopened, payment declined. It comes in as `kind: "system_event"`, with `speaker` set to `system` and a `canonical_type` such as `invoice.credited`, plus structured fields in `fields`.

There are three ways in:

| Way in                                        | When to use it                                                                                                                                                                                                          |
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Generic webhook](/en/guides/system-webhooks) | The system already sends webhooks. Point it at `POST /v1/ingest/webhook/{source_id}`, and a versioned mapping turns the payload into events                                                                             |
| File import                                   | The system has no webhook and exports files: send a JSONL of batch items to [`POST /v1/ingest/files`](/en/api/ingest-files), up to 512 MB, and follow it with [`GET /v1/ingest/files/{import_id}`](/en/api/ingest-file) |
| API                                           | Your code already knows about the event and sends it through the SDK or `POST /v1/batch`                                                                                                                                |

<CodeGroup>
  ```python Python theme={null}
  from niadra import Niadra, system_id

  niadra = Niadra()

  niadra.track({
      "kind": "system_event",
      "channel": "erp",
      "canonical_type": "invoice.credited",
      "idempotency_key": "erp-inv-0823-credit-1",
      "handles": [system_id("crm", "48213")],
      "object_refs": [{"type": "invoice", "namespace": "erp", "id": "0823"}],
      "speaker": {"role": "system"},
      "fields": {"amount": 40.0, "currency": "USD"},
  })
  ```

  ```typescript TypeScript theme={null}
  niadra.track({
    kind: "system_event",
    channel: "erp",
    canonical_type: "invoice.credited",
    idempotency_key: "erp-inv-0823-credit-1",
    handles: [handles.systemId("48213", "crm")],
    object_refs: ["invoice:erp:0823"],
    speaker: "system",
    fields: { amount: 40.0, currency: "USD" },
  });
  ```

  ```bash cURL theme={null}
  curl -X POST "https://acme-prod.us-east-1.api.niadra.com/v1/batch" \
    -H "Authorization: Bearer $NIADRA_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"items": [{"type": "event", "kind": "system_event", "channel": "erp",
         "canonical_type": "invoice.credited", "idempotency_key": "erp-inv-0823-credit-1",
         "handles": [{"type": "system_id", "value": "48213", "scope": "crm"}],
         "object_refs": [{"type": "invoice", "namespace": "erp", "id": "0823"}],
         "speaker": {"role": "system"}, "fields": {"amount": 40.0, "currency": "USD"},
         "occurred_at": "2026-09-22T17:06:21Z"}]}'
  ```
</CodeGroup>

A system event is structured data and enters **without an AI model**: the mapping pulls out the type, the object, the customer ids, the fields and the time. Only mapped types enter the memory, which shields it from the volume of an ERP. The raw payload of an unmapped type is kept for 7 days so you can remap it, and never enters the memory. Free text from inside a system, such as a ticket description, comes in as a `message` on its own channel and goes through normal extraction.

System events are not billed. The unit is still the conversation or task.

## Business objects

Orders, tickets, invoices, contracts, deliveries and subscriptions are **objects**. Each is identified by the triple `type`, `namespace` and `id`, unique within the space, such as `invoice:erp:0823`. The SDKs accept that shorthand.

An object is tied to the customer through their id in that system, the `system_id` handle, and keeps the timeline of events and actions plus a **derived state**, always with `as_of`, the source and a reference to the source record. **The official value stays in your system.** The memory keeps enough for the agent to remember and act, and points back to the source. Splitting two profiles takes orders and invoices to the right owner on its own, because the object is tied to its handle of origin.

Read an object with [`GET /v1/objects/{object_type}/{namespace}/{external_id}`](/en/api/object) and its events and actions with [`/timeline`](/en/api/object-timeline), both with the `context` scope and a receipt. The ids may contain slashes and colons. In the SDKs, `object_state()` and `object_timeline()` in Python, `objectState()` and `objectTimeline()` in TypeScript:

<CodeGroup>
  ```python Python theme={null}
  invoice = niadra.object_state("invoice:erp:0823")
  if invoice:
      print(invoice.state, invoice.as_of, invoice.open_items)
  ```

  ```typescript TypeScript theme={null}
  const { data: invoice } = await niadra.objectState("invoice:erp:0823");
  if (invoice) console.log(invoice.state, invoice.as_of, invoice.open_items);
  ```

  ```bash cURL theme={null}
  curl "https://acme-prod.us-east-1.api.niadra.com/v1/objects/invoice/erp/0823" \
    -H "Authorization: Bearer $NIADRA_API_KEY"
  ```
</CodeGroup>

Your governance team sees every object of one customer, with the actions on them, through [`GET /v1/profiles/{profile_id}/objects`](/en/api/profile-objects).

## Agent actions

An action is the record of what an agent, internal or customer-facing, or a human attendant did in a system: the canonical operation (`credit`, `reschedule`), the object, the result, the purpose and, optionally, `closes`, the open item the action fulfils.

<CodeGroup>
  ```python Python theme={null}
  niadra.action(
      "credit",
      subject=system_id("crm", "48213"),
      object="invoice:erp:0823",
      result="$40 credit on the August bill",
      closes={"object": {"type": "invoice", "namespace": "erp", "id": "0823"}, "operation": "dispute"},
      channel="erp",
      task_id="billing-7741",
  )
  ```

  ```typescript TypeScript theme={null}
  niadra.action({
    channel: "erp",
    task_id: "billing-7741",
    handles: [handles.systemId("48213", "crm")],
    object_refs: ["invoice:erp:0823"],
    operation: "credit",
    result: "$40 credit on the August bill",
    closes: { object: { type: "invoice", namespace: "erp", id: "0823" }, operation: "dispute" },
  });
  ```
</CodeGroup>

`closes` points to the open item by `item_id` or by the object and operation pair, never both. Recording an action needs the key's `act` scope, for that operation and object type; `track` alone is not enough. Actions are immutable: a correction is a new action with `corrects_action_id`.

### Declared, confirmed, divergent

| State       | When                                                                                                                                                         |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `declared`  | The agent recorded it and the system of record has not confirmed yet. The context says so: "declared by Billing at 2:06 pm; not yet confirmed by the system" |
| `confirmed` | The confirming system event arrived, such as `invoice.credited`. The action and the event become one record, and the credit is never counted twice           |
| `divergent` | The window expired with no confirmation. A [trigger](/en/concepts/triggers-and-webhooks) can notify you                                                      |

A declared action only closes an open item when its operation is in the `trusted_action_ops` of the source; a confirmed action always closes it. Actions from a session with an excerpt flagged as prompt injection are quarantined: they close nothing and never show as done.

### Retroactive close

An action may arrive before the open item exists. At 2:05 pm Marina disputes the invoice in the app, and the open item is only born when the app session closes and is extracted, at 2:35 pm. The billing agent acts at 2:06 pm. When it creates the open item, Niadra looks for actions already applied to the same object and operation within the window, and the open item is born `resolved`, closed by the 2:06 pm action.

## Context per task

The internal agent reads context centered on the object, with a task view and the `no_customer` level, because no customer is present:

<CodeGroup>
  ```python Python theme={null}
  with niadra.task(
      "billing-7741",
      object="invoice:erp:0823",
      view="task:billing",
      verification="no_customer",
      channel="erp",
  ) as task:
      ctx = task.context()
      # the agent decides and posts the credit in the ERP, with its own credentials
      task.action("credit", result="$40 credit on the August bill")
  # leaving the block sends task.ended
  ```

  ```typescript TypeScript theme={null}
  const task = niadra.task({
    task_id: "billing-7741",
    channel: "erp",
    object: "invoice:erp:0823",
    view: "task:billing",
    verification: "no_customer",
  });
  const ctx = await task.context();
  // the agent decides and posts the credit in the ERP, with its own credentials
  task.action({ operation: "credit", object_refs: ["invoice:erp:0823"], result: "$40 credit on the August bill" });
  await task.end();
  ```
</CodeGroup>

A task view favours objects of the task's type, the open items tied to them and what was said about them in conversations. Whatever the source's purpose does not allow stays out: the billing agent reads invoices and disputes, not technical open items.

## The 2:06 pm path

1. **2:05 pm, app:** the dispute comes in and, in under 1 second, is readable and tied to `invoice:erp:0823`.
2. **2:06 pm, billing agent:** reads the invoice context, gets the dispute through `live`, posts the credit in the ERP and records the action.
3. **2:06 pm, ERP:** emits `invoice.credited`; the action becomes `confirmed`.
4. **Within seconds:** the voice context gains "done by another agent: \$40 credit, confirmed by the system".
5. **2:07 pm, call:** the voice agent says the credit is already applied.

No AI model sits between the action and the voice context. Actions and system events reach the recompiled context in under 10 seconds.

<Note>
  Niadra never writes to a system of record. The agent acts, with its own credentials. To send the outcome back to your CRM, consume the `action.recorded` and `open_item.closed` webhooks.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Internal agents" href="/en/guides/internal-agents">
    the full guide to the billing agent.
  </Card>

  <Card title="Webhooks from your systems" href="/en/guides/system-webhooks">
    mapping and authentication.
  </Card>

  <Card title="Read an object" href="/en/api/object">
    derived state and open items.
  </Card>
</CardGroup>
