> ## 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.

# Internal agents

> The billing agent reads the invoice context, posts the credit in the ERP and records the action.

Internal agents work inside your systems without talking to the customer: billing reviews disputes and posts credits, orders reschedules deliveries in the ERP, tickets triages the help desk. They act on the same customers your service agents talk to, and without a shared memory each side gets it wrong in its own way. The internal agent acts without knowing what was said in service; the service agent promises what another agent already did.

This guide wires a billing agent to Niadra with the same calls a service agent uses: it reads the context of the invoice it works on, posts the credit in the ERP with its own credentials, and records the action. A minute later the voice agent answers knowing it.

## The flow at 2:05, 2:06 and 2:07 pm

1. **2:05 pm, app.** Marina disputes the August bill in your app. The message is tied to `invoice:erp:0823` through `object_refs` and is in the live layer in under a second.
2. **2:06 pm, billing agent.** It reads `context(object="invoice:erp:0823", view="task:billing", verification="no_customer")`, receives the dispute through `live`, the technician visit that failed at 2:02 pm and the March credit from the history. It posts the \$40 credit in the ERP and records `action("credit", closes=...)`.
3. **2:06 pm, ERP.** The ERP emits `invoice.credited`. The action and the event become one record, and the action goes from `declared` to `confirmed`.
4. **2:07 pm, call.** The voice pack already says: "Done by another agent: \$40 credit on the August bill, Billing, 2:06 pm, confirmed by the system".

No language model sits between the recorded action and the voice context. Actions and system events reach the live layer in under a second and the recompiled pack in under ten.

<Note>
  Niadra never executes an action in a system of record. Your agent acts, with its own credentials. Niadra keeps what it needs to remember and what it did.
</Note>

## Before you start: the source

An internal agent is a source like any other, with its own key, audience class `internal_agent` and a purpose such as `billing`. Access to the ERP is not access to the memory: the billing agent reads invoices and disputes, not technical open items or health data. Two settings matter here:

* **Scope `act`** on the key, and `trusted_action_ops` on the source (here `["credit"]`): the closed list of operations it may record, and the ones whose declared actions close open items before the system confirms them. The `track` scope alone never records an action.
* **Verification `no_customer`**, the level for tasks with no customer present. It sits outside the V0 to V4 scale and is only accepted from `internal_agent` sources. A pack built for `no_customer` never goes to the customer.

Both are set on the source through the [control API](/en/api/control/sources-create) or the Console.

## Steps

### 1. Open a task centered on the invoice

A task plays the role a conversation plays for service agents: it pins the pack, scopes the SDK cache, groups the events for billing and closes with `task.ended`. Without it, the server closes the task after 10 minutes of inactivity.

The `task:billing` view is defined by your space as a template plus a policy. It favors objects of the task type, the open items tied to them and what was said about them in conversations; it leaves out what the billing purpose does not allow.

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

  niadra = Niadra(channel="erp")

  with niadra.task(
      "billing-7741",
      subject=system_id("crm", "48213"),
      object="invoice:erp:0823",
      view="task:billing",
      verification="no_customer",
      agent_id="billing-agent",
  ) as task:
      ctx = task.context()
      decision = billing_llm.review(instructions=BILLING_RULES, context=ctx.system_block, recent=ctx.turn_block)
  ```

  ```typescript TypeScript theme={null}
  import { Niadra } from "@niadra/sdk";

  const niadra = new Niadra();

  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();
  const decision = await billingLlm.review({ instructions: BILLING_RULES, context: ctx.text, recent: ctx.suffix });
  ```

  ```bash cURL theme={null}
  curl -X POST "https://acme-prod.us-east-1.api.niadra.com/v1/context" \
    -H "Authorization: Bearer $NIADRA_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "object": { "type": "invoice", "namespace": "erp", "id": "0823" },
      "view": "task:billing",
      "verification": "no_customer",
      "task_id": "billing-7741"
    }'
  ```
</CodeGroup>

`context(object=...)` resolves the invoice to its owner and returns the pack centered on it: the timeline of the object plus the customer context the task needs. The same laws hold as for a conversation: precompiled, pinned by task, with ETag and a receipt of the read.

### 2. Act in the ERP, then record the action

Post the credit through your ERP integration. Then record the action with `closes`, the open item it fulfils. Name it by id, or by the object and a canonical operation, which is what you usually know: the dispute on invoice 0823.

<CodeGroup>
  ```python Python theme={null}
      erp.post_credit(invoice="0823", amount=40.00, currency="USD")  # your integration, your credentials

      task.action(
          "credit",
          result="$40 credit on the August bill",
          purpose="billing",
          closes={"object": {"type": "invoice", "namespace": "erp", "id": "0823"}, "operation": "dispute"},
      )
  ```

  ```typescript TypeScript theme={null}
  await erp.postCredit({ invoice: "0823", amount: 40.0, currency: "USD" }); // your integration, your credentials

  task.action({
    object_refs: ["invoice:erp:0823"],
    operation: "credit",
    result: "$40 credit on the August bill",
    purpose: "billing",
    closes: { object: { type: "invoice", namespace: "erp", id: "0823" }, operation: "dispute" },
  });
  await task.end();
  ```
</CodeGroup>

An action is immutable, like every event. A correction is a new action with `corrects_action_id` pointing to the old one.

### 3. Let the system of record confirm it

The action stays `declared` until an event from the system of record confirms it. Send the ERP events through the [generic webhook](/en/guides/system-webhooks) or as system events. When `invoice.credited` arrives for the same object and operation within the window, the two become a single record and the action is `confirmed`, so the credit is never counted twice.

| Status        | What it means                                          | What the pack says                                                                    |
| ------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------- |
| `declared`    | Recorded by the agent, not yet confirmed by the system | "Declared by Billing at 2:06 pm; the system has not confirmed yet"                    |
| `confirmed`   | The system of record emitted the matching event        | "Confirmed by the system"                                                             |
| `divergent`   | The window passed without the confirming event         | Flagged, and the `fact.contradicted_by_system_event` trigger condition can notify you |
| `quarantined` | Recorded in a session flagged for prompt injection     | Never shown as done and closes nothing                                                |

A declared action only closes an open item if its operation is in the `trusted_action_ops` of the source; a confirmed one always closes it.

### 4. Understand the retroactive close

The action can arrive before the open item exists. The dispute written in the app at 2:05 pm only becomes an open item when the app session closes and is extracted, say at 2:35 pm, and the billing agent acted at 2:06 pm. When the open item is created, Niadra looks for actions and events already applied to the same object and operation within the window. If one exists, the open item is born `resolved`, pointing to the action that closed it.

### 5. Let other agents know

Every agent that reads this customer next sees the action in "Done by another agent", with its source and time. If your CRM needs to show the outcome, subscribe to the `action.recorded` and `open_item.closed` webhooks and write it there with your own integration. See [Triggers and webhooks](/en/concepts/triggers-and-webhooks).

## Delta for recurring work

Internal agents often revisit the same customers. Ask with `delta=True` (`delta: true`) and the answer carries only what changed since this source last read the customer: "what changed since the last time YOU looked" costs tens of tokens.

## Next steps

<CardGroup cols={2}>
  <Card title="Systems, objects and actions" href="/en/concepts/systems">
    objects, derived state and actions in depth.
  </Card>

  <Card title="Webhooks from your systems" href="/en/guides/system-webhooks">
    how `invoice.credited` gets in.
  </Card>

  <Card title="Voice agents" href="/en/guides/voice-agents">
    the call that knows about the credit.
  </Card>

  <Card title="Context use" href="/en/concepts/context-use">
    how the action counts as use of the context.
  </Card>
</CardGroup>
