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

# SAP OData

> Read, query, create, update, delete or run an action on any entity in a SAP OData V2 or V4 service.

Reads and writes one configured entity in a SAP OData service, **V2 or V4** — sales orders, purchase orders, business partners, or anything else the connected system publishes — without a purpose-built tool for each one. The tool works out which protocol a service speaks from its own `$metadata`; nothing asks you to choose, because one SAP system serves both and the same ABAP service can be bound as either.

The service, entity and operation are fixed when you set the tool up, along with any field values you pin there. The agent supplies only the business values you leave to it.

## Authentication and enablement

Requires a **SAP** connection under **Connectors**, using Basic Auth or OAuth 2.0 (client credentials). The base URL, SAP client, service path and credentials all come from that connection; nothing about the target system is ever supplied by the agent.

Off by default. Add it to an agent from the **Tools** tab; it has its own full-page setup flow (see Configuration) and cannot be saved until that flow is complete. A tool that has not been configured fails with "this tool has not been configured yet".

### Connection fields

| Field | Stored as | What to enter |
| - | - | - |
| **SAP Base URL** | `base_url` | The **host root**: scheme, host and port, for example `https://sap.example.com:44300`. Not the service path. A reverse-proxy prefix in front of the SAP paths may stay; an OData service path pasted onto the end (the default or the configured one) is stripped. |
| **SAP Client** | `sap_client` | Required. The client (MANDT), for example `100`. Sent as `sap-client` on every request. |
| **OData Service Path** (Advanced settings) | `odata_path_v2` | Where V2 services named by a bare name live. Default `/sap/opu/odata/sap`. Change it when services are published outside the standard namespace, for example `/sap/opu/odata/ZCUST`. Blank means the default. |
| **Service Catalog Path** (Advanced settings) | `catalog_path_v2` | The Gateway V2 service catalog, used only to suggest service names while you configure a tool. Default `/sap/opu/odata/IWFND/CATALOGSERVICE;v=2/ServiceCollection`. **Blank means catalog search is off** for V2: that catalog is never called. A connection saved before this field existed keeps the default. |

Both paths must be absolute paths starting with `/`. A full URL, a query string or fragment, spaces, or a bare `/` is rejected when the connection is saved. The **Service Catalog Path (V4)** (`catalog_path_v4`) is a third Advanced field on the same terms — it defaults to `/sap/opu/odata4/iwfnd/config/default/iwfnd/catalog/0002/ServiceGroups`, and blank turns V4 catalog search off. There is no V4 *service* path field: a V4 service is addressed by its whole path, so there is no prefix to override. A tool run never reads any catalog.

## Choosing the service

A service is identified by one of two forms:

* A **bare name** such as `API_SALES_ORDER_SRV` (a V2 service), joined to the OData Service Path. Keep any `;v=2` suffix the catalog shows; it is part of the identifier.
* A **path**: anything containing a `/` is taken as an absolute path from the host root. This is how a V4 service is addressed (`sap/opu/odata4/sap/<group>/srvd/sap/<service>/0001`), and also a V2 service in a customer namespace or behind a middleware address.

A URL (anything containing `://`), a `..` segment, whitespace, `?`, `#` or `\` is refused.

The picker suggests services from the catalogs, but the catalog is a suggestion, not a gate. A service is usable if its `$metadata` can be read, whether or not any catalog lists it. Typing a service name and verifying it always works, and so does pasting a working curl command (see Configuration), including when both catalogs are unreadable.

How the catalogs are searched:

* **V2:** the text you type is pushed down to SAP as a name filter on `TechnicalServiceName`, so a search reads a handful of rows instead of every published service.
* **V4:** the catalog cannot be narrowed on the server, so the whole list is fetched once and filtered locally.
* **Release status and service type** travel with each row and are narrowed locally in both. The V2 gateway answers such filters with an empty list and the V4 catalog ignores them, so they are never sent.
* Each version returns 100 services per page, and pages accumulate as you scroll. A catalog is cached for 10 minutes, failures included, keyed on the connection's identity — its host, its OData and catalog paths, its SAP client and its credentials — so two connections to the same host do not share one another's catalog.

When a catalog cannot be read, the service list says why for that catalog (`v2_reason`, `v4_reason`) and still returns whatever the other one gave. It reports `available: false` only when neither answered.

| Catalog | Reason given | Meaning |
| - | - | - |
| V2 | `the V2 service catalog is not configured on this connection` | The Service Catalog Path is blank. Deliberate; the V2 catalog was not asked. |
| V2 | `the service catalog returned <status> — it is often not activated or not authorised for this user` | `/IWFND/CATALOGSERVICE` is not activated, or the user may not read it. |
| V4 | `the V4 service catalog returned <status> — it is published separately from the V2 one (transaction /IWFND/V4_ADMIN) and is often not activated` | The V4 catalog is a separate service and is commonly inactive. |
| Either | `reach the service catalog: …`, `reach the V4 service catalog: …` | The request did not get an answer. |
| Either | `read the service catalog: …`, `read the V4 service catalog: …` | The answer was not a catalog. |

If the first page of a catalog fails, that catalog counts as unavailable. If a later page fails, a next-page link cannot be followed, or the 50-page limit is reached, the rows already read are kept and `v2_partial` or `v4_partial` says why, so a service missing from the list may simply not have been read. A partial catalog is cached for one minute, not ten. When neither catalog answered, the single `reason` is the V2 one, unless V2 is only "not configured", in which case it is the V4 one.

## Configuration

A SAP OData tool is set up in its own dialog, because what it asks depends on the entity you pick. Open it from the SAP OData tool in the **Tools** tab: **Configure parameters** the first time, **Add configuration** for further ones, and **Edit configuration** (or the configure icon) on an existing one. As with a custom connector, **Edit parameters** on an existing configuration opens the native parameter editor instead — the name, description and SAP connection — with an **Edit configuration** button through to the dialog.

There are three steps. **Next** is enabled when the current step is complete, and a finished step can be reopened from the header. While you work, the configuration is re-checked against the service's live `$metadata`, and everything you are offered — entities, field names, labels, lengths, the operations an entity permits — comes from that document. A saved tool that no longer compiles (a field was renamed in SAP, or the connection changed) reopens on the step that explains why. Closing with unsaved changes asks you to confirm.

| Step | You decide | Next is enabled when |
| - | - | - |
| **1. Source** | The tool's name, which SAP connection, which OData service, which entity set, and what the tool does to it. | The tool is named, the service's `$metadata` has been read, and an entity and an operation (and, for an action, which one) are chosen. |
| **2. Fields** | What goes out to SAP and who supplies each value, and what comes back — one list, both answers per field. | Every required field is set or exposed, a delete is explicitly allowed, and the configuration compiles. |
| **3. Result Card** | How a run of the tool looks in the feed. | The configuration compiles. |

### 1. Source

Give the tool a **name** (saved in snake\_case) and an optional **description** saying when the agent should reach for it. Then pick the SAP connection, explicitly: on a workspace with a sandbox and a production system, every entity and field below is read live from the one you choose. When the workspace has only one SAP connection it is picked for you.

Choose the service: search the catalogs, type a name and verify it (see Choosing the service), or open **Start from a call you already have** and paste a curl command. The service, entity, operation, filters, keys, `$select`, `$expand`, `$top`, `$orderby` and request body are read out of the command and filled in, where every one of them stays editable. The address in the command is ignored — the connection supplies it — so a call copied from any system configures a tool against the connected one. Anything a configuration cannot express (a filter that is not a conjunction of equalities, `$skip`, `$search`) is listed as not carried over rather than silently dropped. Once the description is read, the step shows how many entity sets the service publishes and whether it speaks OData V2 or V4.

Then the entity and the operation:

* **Entity set.** A searchable picker. The search runs on the server and matches the name or the label: every word you type must appear, in any order and case, so "order item" finds `A_SalesOrderItem`. It shows the first 50 matches with "Showing 50 of N — type to narrow the list". The chosen entity stays visible however the list is narrowed, with its property count, related-set count and key, and a **draft-enabled** tag when it is a draft-enabled set.
* **Operation.** Query (many rows by filter), Read (one row by key), Create, Update (patch by key), Delete, or **Action**. Operations the entity set does not permit are listed as "not permitted" and cannot be chosen. A read-only entity offers no Create or Delete, and Action appears when the service declares operations bound to that entity (see below).
* **Operation to run** (Action only). The service's own operations for that entity, each described by whether it reads without changing anything, whether it creates a new record, and what it returns.
* **Include drafts in the results.** For a query on a draft-enabled set. Off, the query returns active records only, which is what "the sales orders" means; on, it returns drafts as well.

### 2. Fields

One table, two answers per field. The heading follows the operation: **Filters and response** for a query, **Key and response** for a read, **Payload and response** for a create, **Key, fields and response** for an update, **Key** for a delete, **Parameters and response** for an action. The table opens on every field, with **In use only** to narrow it to the ones this tool uses and **Show all** to go back.

**Who provides it** decides what goes out:

* **Set here** (`bind`) — you pin the value; the agent never sees the field.
* **Agent fills** (`agent`) — the field stays in the agent's tool schema.
* **Not used** (`off`) — the field is left out entirely.

On a query, the next column is **Filter**. A pinned field shows its value box. A field the agent fills offers **Exact value** (equality, the default) or, on a date, timestamp or number, **From … to (range)**, which gives the agent two optional inclusive arguments, `<Field>_from` and `<Field>_to`, instead of one. On a Read or Delete only the key is sent: the other fields are disabled, with a tooltip saying they are not sent. Fields SAP will not accept for the chosen operation are shown locked, with the reason (for example `sap:creatable="false"` on Create). A field SAP requires must be set or exposed: the step names what is missing and **Next** stays disabled. A **Query** must leave at least one field on **Agent fills**, or **Next** stays disabled for that reason too — a query with nothing the agent can narrow on is one call with one answer, the same rows on every run whatever the agent was asked.

**In response** decides what comes back, for every operation but Delete. The ticks become the request's `$select`, so an unticked field is never fetched rather than fetched and discarded. A level you do not touch uses the service's own description (up to 12 identifying fields for the header, the key first, and up to 6 columns for a table), and those defaults are ticked for you so the boxes show what the tool will actually do.

Related record sets are nested inside the same table, because they are part of the same document rather than a separate list of extras. Add one with **Add related data**; ticking a field inside it is what puts the `$expand` on the request. They can nest as deep as the service allows, up to 6 navigations (the dialog offers three levels), so a sales order's items, their schedule lines and their pricing conditions are all one configuration. On a **Create** an insertable set is also written inline in the same request, with its own **Who provides it** column, so the whole document is created or none of it. A set the service marks **not** insertable can still be added: it is read back after the write rather than written with it. Every create and update reads the document back afterwards, which is why a created order shows its items instead of a bare header.

Below the table, for a Query, a row of controls sets **Maximum rows** (default 50, at most 500), **Sort by** (only fields SAP marks sortable are offered, ascending or descending), **Total match count**, which lets the agent say "12 of 340" at the cost of an extra count on the SAP side, and on a V4 service **Free-text search**, which adds a `search` argument sent as `$search`. On a draft-enabled entity, **Include drafts** is offered there too; off, the query returns active records only.

**Delete is off by default.** A delete tool shows a single control, **Allow this agent to delete in SAP**, and cannot be saved without it (`allow_delete`).

Saving compiles the configuration against the service's live `$metadata`: the parameters the agent will be asked for are worked out and frozen into the tool then. A configuration the service would reject is refused here, naming the field or the annotation. At run time the tool re-checks the configuration against the service's current `$metadata` (cached for up to an hour), so a field removed by a later upgrade fails clearly rather than producing a bad request.

### 3. Result Card

The card is how a run looks in the feed: HTML in the feed renderer's Handlebars subset, drawn by the same component the feed uses, so the preview (built from sample values: the shapes are real, the data is not) is what a run shows.

The step opens on the **built-in card**: a template derived from the fields you ticked, in the shape of the product's other SAP cards — a header bar with the SAP mark (or your **Logo** URL) and the key as a badge, a two-column **Header** grid of boxed values with the rest behind **More**, a collapsible section per related record set, and framed tables that scroll past eight rows with a per-row **Details** holding the columns that did not fit and whatever hangs off the row. A record set beneath a single-valued record (a customer's sales areas under a business partner's customer record) is drawn too. Tabs need a script, which a card may not carry, so related sets stack as collapsible sections instead. It is shown rendered and as its **HTML**, which is yours to edit. Until you take it over it is marked **built-in · follows the fields** and is rebuilt on every save, so a field ticked later appears on the card by itself.

From there, three ways to change it:

* **Generate.** Write a **UI Description** ("lead with the net amount and the sold-to party; show items as a table with the schedule lines under each row") and **Generate** a new card from it; **Regenerate** runs it again. One model call made while you configure; running the tool makes none.
* **Iterate.** Under the preview, **Provide feedback to improve** ("put the status in the header bar", "right-align the amounts") and **Iterate** to have the card on screen improved. Generator warnings — a path the result will not carry — are shown above the preview.
* **Edit the HTML** directly in the text box; the preview follows as you type. **Copy HTML** copies it. **Start Over** or **Reset to built-in** returns to the built-in card.

**Sample JSON Data** shows the exact result the preview is rendered with. A template normally places values from `view` (one record) or `views` (a query's rows), a mirror of the result with every value already formatted: dates in ISO form, an amount followed by its currency and a quantity by its unit, a code followed by its description in parentheses, booleans as Yes and No, and SAP's all-zeros "no reference" values left blank. A V2 `{"results": […]}` expanded collection and a V4 plain array both arrive as a plain array, so one template serves either version. Hidden, draft-bookkeeping and protocol fields are left out, and fields keep the order the service declares them. Every value in the mirror is HTML-escaped. A template may bind only `view`, `views`, the counts (`record_count`, `total_count`), the result flags and `logo_url`. A template that binds the raw `record`, `records` or SAP's message text is refused when the tool is saved, because the card would draw SAP's text unescaped. An expression the renderer does not understand renders as nothing; a helper call, `@index`, `../`, `lookup`, a script or an event handler is refused when the tool is saved.

SAP's messages and run notices are never drawn by the template: the feed shows them around the card.

The step stores four keys on the tool: `template`, `template_source` (`static` for the built-in card, `generated`, or `edited`), `logo_url` and `template_instructions` (the description, kept so a regeneration after the fields change starts from the same words). Each run's result carries the card that was in force when it ran, as `ui_template` and `logo_url`. A template may be at most 64 KB, the description at most 4 KB, and the logo must be an `http` or `https` URL; each is checked when the tool is saved. **Create Tool** (or **Save Changes**) is enabled once the configuration has compiled.

### Filters (Query)

Every query field you set or expose becomes a condition, and all conditions are combined with `and`.

* **Equality.** A field set here pins `Field eq value`. A field the agent fills adds the same condition when the agent supplies a value, and is simply absent when it does not. Fields SAP marks unfilterable are locked. If the entity requires a filter, at least one filter field must be set or exposed; a field SAP marks required-in-filter must itself be set or exposed.
* **Range.** A date, timestamp or number field that the agent fills can be a range (`filter: "range"` in the stored configuration). The agent is then offered `<Field>_from` and `<Field>_to` instead of the field itself; each is optional, and each is inclusive (`ge` and `le`). "Orders since Monday" sends only `_from`. A range is refused at save on anything but a query, on a field that is not agent-filled, that SAP marks unfilterable, or that is not a date, timestamp or number.
* **Search.** A V4 query can offer the agent a free-text `search` argument (`search: true`), sent as `$search`. A single word is sent as is; text containing a space or a quote is sent as a quoted phrase. Nothing is sent when the agent leaves it empty. Refused at save on a V2 service (a V2 gateway answers `$search` with 400) and on anything but a query.
* **Drafts.** A query on a draft-enabled set pins `IsActiveEntity eq true`, because the active row is what "the sales orders" means. **Include drafts in the results** (`include_drafts`) returns drafts as well.

## Actions and function imports

A great deal of SAP business logic has no CRUD equivalent: setting a billing block, updating prices, withdrawing from approval. Choose **Action** as the operation and pick one. The tool handles the three shapes these come in:

* **Bound to one record** — needs the key, exactly as a read does.
* **Bound to the entity set** — a factory, which needs no key. It creates a new document.
* **Unbound** — belongs to the service rather than to any entity.

A parameter is configured exactly like a field: **Set here**, **Agent fills** or **Not used**, with the service's own label and length. A parameter SAP declares mandatory must be pinned or exposed, and the tool cannot be saved otherwise.

* **An action's name may not be unique.** One service can declare the same name bound to the header and again bound to the item. Each is configured separately; the entity you picked says which.
* **Where the result comes from is the action's return type, not the entity it is bound to.** An action that returns nothing gets no card — the result is SAP's own message.

In V2 these are function imports: parameters travel in the query string (no request body, simple types only), and the import's `m:HttpMethod` decides whether it reads or writes. In V4 they are actions and functions: an action is posted with a JSON body, a function is fetched. The configuration is the same either way.

## OData V2 and V4

What differs is only how a request is written and an answer is read, and that is handled for you.

| | V2 | V4 |
| - | - | - |
| Version detected from | `edmx:Edmx` `Version="1.0"` | `Version="4.0"` |
| Service address | one name, `API_SALES_ORDER_SRV` | the whole gateway path, `sap/opu/odata4/sap/<group>/srvd/sap/<service>/0001` |
| Update verb | `MERGE` | `PATCH` |
| Literals in `$filter` and keys | typed: `guid'…'`, `datetime'2024-05-01T00:00:00'`, `datetimeoffset'…'`, `time'…'`, `10.5m` | bare: unquoted GUIDs, `2024-05-01`, `2024-05-01T00:00:00Z`, plain numbers |
| Nested records on create | `{"results": […]}` | a plain array |
| Total count | `$inlinecount=allpages` (answered as a string in `__count`) | `$count=true` (answered as a number in `@odata.count`) |
| Next page | `__next`, usually an absolute URL | `@odata.nextLink`, which may be absolute, rooted at the host, or relative |
| Business messages | `sap-message` response header | `sap-messages` response header (a JSON array, `numericSeverity` 1 to 4 for success, info, warning, error) and a `SAP__Messages` property on the returned entity |
| Error detail | `error.innererror.errordetails[]` | `error.details[]`, with `target` and severity |
| Concurrency token | a property with `ConcurrencyMode="Fixed"`; token from the `ETag` header or `__metadata.etag` | `Core.OptimisticConcurrency` on the entity; token from the `ETag` header or `@odata.etag` |
| Row-bound action | function import annotated `sap:action-for` | bound action posted to `Entity(key)/Namespace.Action` |
| Free-text search | not available (answered with 400) | `$search`, when configured |
| Draft handling | not applicable | draft-enabled sets (see below) |
| Field metadata | `sap:label`, `sap:unit`, `sap:creatable` … | `SAP__common.Label`, `SAP__measures.ISOCurrency`, `Core.Computed` … |

Notes that apply to both:

* **Concurrency.** Updating, deleting or running a row-bound action on an entity that declares a concurrency token reads the row first and sends its real token as `If-Match`, never `*`. Without it SAP refuses the write with `428 Precondition Required`. An action bound to the whole set has no row and needs no token. If the read fails, the call stops with an error that says so.
* **Messages.** Messages are returned on success as well as failure, because a created document routinely carries a warning. The tool reads both message headers and lifts `SAP__Messages` out of every returned record, so it never shows up as a field.
* **Errors.** When SAP's error has per-item detail, those lines are shown in preference to the generic outer message.

## Draft-enabled entities

A V4 entity set is draft-enabled when `IsActiveEntity` is part of its key. Every row of such a set can exist twice, as the active record and as a draft that only its author sees, and nothing is done until the draft is activated. The tool runs the lifecycle for you; `IsActiveEntity` and the other draft bookkeeping fields are never offered to the agent.

* **Create:** the create answers with a draft. The tool then calls **Prepare** (when the service declares it) and **Activate**, and reads the *active* row back.
* **Update:** **Edit** the active row into a draft (with `PreserveChanges=true` when the Edit action takes that parameter, and with `If-Match` on a guarded entity), **PATCH the draft**, Prepare when declared, **Activate**, then read the active row back.
* **On failure:** if Prepare, Activate or the change to the draft fails, the tool calls **Discard** and reports `NN/DRAFT_NOT_ACTIVATED` as an error message. If Edit itself fails there is no draft to discard. Either way the call returns an error, with `draft` and SAP's messages on the result.
* **Activate with no answer:** if Activate gets no answer, or a 502, 503 or 504, the tool checks before discarding anything. If the draft is gone and the active row is there, the activation went through and the result says so with `NN/DRAFT_ACTIVATE_UNANSWERED`.
* **Draft nodes are refused.** An item under a draft root has no lifecycle of its own and is created, changed and activated only through its root, so a Create, Update or Delete configured on a node is rejected when you save it.
* **Activation is marked unverified.** The create-as-draft and Discard exchanges have been observed against a live service. The Prepare, Edit and Activate exchanges follow SAP's published draft contract and have not, so every draft result carries `draft.unverified: true`. Check `draft.activated` and the record read back before relying on the outcome.

## Inputs

The agent-visible parameters depend on the configured entity and operation — they are the fields you left as **Agent fills**, named exactly as SAP names them, with the service's own labels as their descriptions, SAP's own field lengths, and dates typed as dates. A field pinned with **Set here** or switched off is not in the agent's parameters at all, and a query filter SAP marks unfilterable is left out rather than offered and rejected.

| Operation | What the agent supplies |
| - | - |
| Query | Any filter fields you exposed (all optional), `<Field>_from` / `<Field>_to` for a range field, and `search` when enabled. |
| Read | The entity's key. |
| Create | The exposed fields, plus rows for any insertable related record set you added. |
| Update | The key, plus the fields you exposed. |
| Delete | The key. |
| Action | The exposed parameters, plus the key when the action is bound to one record. |

## Outputs

| Field | Description |
| - | - |
| `record` / `records` | The row for Read, Create, Update and row-returning Actions; the rows for a Query. Dates are normalised to ISO. |
| `record_count`, `total_count` | Rows returned, and the unpaged total when requested. |
| `truncated` | There are more rows than came back: the row limit was reached, SAP still had a page to give, or paging stopped early (`partial_reason` says why in that case). |
| `pages_fetched`, `partial_reason` | How many server pages were read, and why reading stopped early if a later page failed or the paging budget ran out. |
| `messages` | SAP's own business messages, each with `code`, `message`, `severity` (`success`, `info`, `warning`, `error`), and where SAP gave them `target`, `transition`, `longtext_url` and nested `details`. Returned on success too. |
| `highest_severity` | The worst severity among `messages`. |
| `succeeded_without_effect` | SAP accepted the request and reported an error: the call worked, the business operation did not. Set when `highest_severity` is `error` on a call that otherwise succeeded. |
| `draft` | For a draft-enabled entity, what the lifecycle did: `created`, `edited`, `activated`, `discarded`, `draft_key`, `unverified`. |
| `deleted`, `updated` | Set for a successful Delete or Update. |
| `data_fetch_warning` | The write succeeded but its answer, or reading the document back, could not be read, so the record shown may be incomplete or absent. Do not repeat the write. |
| `outcome_unknown` | A Create, Update, Delete or Action got no answer: the connection dropped, or a gateway in front of SAP returned 502, 503 or 504. The call is an error, but SAP may have done the work, so read or query before repeating it. |
| `key_padded` | The key had to be zero-padded to match how SAP stores it. |
| `view`, `views` | The record (or, for a query, each row) with every value already formatted for display — ISO dates, an amount with its currency, a code with its description, Yes/No — and a V2 `{"results": […]}` collection flattened to an array. The designed result card is drawn from these. |
| `ui_template`, `logo_url` | The card template and logo the call was drawn with — a designed card as it was stored, or the built-in one derived fresh from this configuration on each run, so it never goes stale against the fields and never outgrows the stored template's size cap. Absent only on results recorded before templates existed; the feed says so rather than drawing nothing. |
| `card` | The built-in result card layout, derived from the service's metadata. The `layout` key alongside it carries the maker's own arrangement of that card — the card title, renamed labels, hidden fields and the drag order on the **Result Card** and **Fields** steps all write it — and it is applied to the built-in card and to the markdown summary. |
| `tool_name`, `service`, `entity`, `operation`, `action`, `version`, `completed_at` | Which configured call ran, the protocol it spoke (`v2` or `v4`), and when it finished. |

### Keeping large reads out of the agent's context

On by default, for **Query and Read only**: a successful read stores its structured result as a task variable, so the agent hands the rows to another tool as `${VAR_N}` instead of carrying them in every later turn. On an agent with `harness_v2`, a result over 1 KB is then collapsed to that reference plus a short preview, which is where the context saving comes from — a query that returned two hundred rows stops being re-sent on every decision.

The card keys (`ui_template`, `logo_url`, `view`, `views`, `card`) never reach the model at all; they are feed-only, and the variable is stored without them. The full result always stays on the step, whatever this is set to.

Writes — Create, Update, Delete and Actions — register no variable: their answer is the record just written, which is small, and what matters about it is that it happened.

To keep reads inline only, bind `store_result_as_variable` to `false` on a configured tool. The binding is hidden from the agent, and any value other than `true` or `false` is rejected when saved. Set it through the API or the platform MCP's `update_configured_tool`; the configured-tool dialog keeps it when the tool is saved again.

Alongside that structured result, every call returns a short markdown summary — the record's header fields, its related record sets as tables, and any SAP message. It follows the card layout derived from the service's metadata. This is what the task summary is written from. Rows are capped (20 for a query, 25 per related set, cells cut at 60 characters) and whatever is left out is counted, never dropped silently. When SAP reported an error on an accepted request, the summary says to treat the operation as not done.

## Limits

* **Rows.** A query returns the Maximum rows you set, default 50, ceiling 500.
* **Paging.** Server-driven paging is followed until the row limit is met, at most 20 pages. After the first page, paging stops once 2 minutes have been spent; the rows already read are returned with `truncated: true` and a `partial_reason` of the form `stopped after N page(s): the 2m0s budget for one call was spent`. The first page is never subject to the budget. A next-link is used exactly as SAP sent it and must point at the connection's own host.
* **Retries.** Each request has a 60-second timeout and is retried up to 3 times on a network error or an HTTP 429, 500, 502, 503 or 504, with exponential backoff (roughly 2, 4 then 8 seconds, with jitter, never more than 10). Writes are included.
* **Session token.** Every write first establishes a fresh CSRF token by reading `$metadata`, so a token is never carried over from an earlier call. A write refused with 403 and `x-csrf-token: Required` is retried once with a freshly fetched token.
* **Depth.** Related records follow up to 6 navigations. Deeper is refused when you save.
* **One operation per tool.** Configure two tools if you need to read and create.
* **Filters.** Conditions combine with `and`, on equality, plus the inclusive `ge` / `le` bounds of a range. There is no `or`, no other comparison, no function and no `$skip`. This is deliberate: classic SAP gateway services flatten a filter into ABAP select-options, which cannot express `or`, and several implementations respond by returning the **unfiltered** set.
* **Related records on create** are limited to what the service marks insertable, which is not always the same as what its entity sets allow.
* **Imported calls** carry `$filter` only where it is equality clauses joined by `and`. An `or`, a comparison or a function is reported, not partially applied.
* **Messages.** Each message header value over 64 KB is skipped, and at most 200 messages are kept per response; when that drops anything, an `NN/TRUNCATED` info message says so.
* **Key padding.** A not-found on a Read, Update or Delete whose key is all digits and shorter than the field is retried once with the key zero-padded; the result then carries `key_padded: true`.
* **Metadata cache.** A running worker re-reads a service's `$metadata` at most once an hour (a failed read is remembered for 30 seconds). The cache is keyed on the same connection identity as the catalog, so a service one connection may not read is never answered from another's copy.

## Side effects

* **Query and Read** are read-only. So is an Action whose function import or V4 function is fetched with GET.
* **Create, Update and Delete**, and any Action that is posted, write to the connected SAP system and are not reversible from here. Pair write operations with an approval step for anything running against production.
* **Extra reads around a write.** A write reads `$metadata` for its session token; an Update, Delete or row-bound Action on a guarded entity reads the row first for its token; and every Create and Update reads the document back afterwards.
* **A write can be sent more than once.** The retry layer re-sends a write that meets a network error or a 429 or 5xx answer, and SAP may already have processed the first attempt. If a create reports a failure, look for the document before repeating it.
* **Drafts.** A draft-enabled write can leave a draft behind. Whenever activation does not happen — it failed, or the service declares no Activate action at all — the tool tries to discard the draft; `draft.discarded` says whether that worked. Only if the discard itself fails does the draft stay open in SAP, where it can hold a lock on the row until someone activates or discards it there.
* **Designing the card.** Designing or refining the result card in the setup flow makes a model call at configuration time. Running the tool makes none.
* **Logging.** Each call records a trace entry with the service, entity, operation and counts only, never field values or URLs, and every request to SAP is audit-logged (host, method and status only, never the path or query).

## Errors

A call can fail in three places: the tool refuses before calling SAP, SAP answers with an error, or SAP accepts the request but reports a problem in `messages`.

| What you see | What it means |
| - | - |
| *this tool has not been configured yet* | The tool was added but its service, entity and operation were never saved. |
| *SAP integration not configured. Please add an SAP integration in the Integrations settings.* | The base URL, credentials or SAP client are missing from the connection. |
| *this tool is not configured correctly: …* | The stored configuration cannot be read, or its service identifier is malformed (for example a URL, or a `..` segment). |
| *could not read the service description: …* | `$metadata` could not be fetched or parsed. See the three causes below. |
| *SAP returned 401 or 403 for … — this connection's user is not authorised for that service* | The connection's user lacks authorisation for the service. |
| *SAP returned 404 for … — the service is not activated on this system* | The name is wrong or the service is not published here. Check any `;v=2` suffix. |
| *response root element is `<html>`, not an EDMX document* | The request was redirected to a logon page or intercepted by a gateway. |
| *this tool's configuration no longer matches … on this system: …* | The system changed under a saved tool: a field it names no longer exists. Re-open the tool and re-save it. |
| *could not read … before updating it, which this entity requires for its concurrency check: …* | The pre-read for the concurrency token failed, so the write was not attempted. |
| *SAP rejected the call (HTTP n): …* | SAP's own error, with its per-field detail where it gave one. |
| *could not read SAP's response: …* | SAP answered with something that is not valid OData JSON. |
| *… is draft-enabled but declares no edit action, so it cannot be changed through this tool* | An Update on a draft entity needs the service's Edit action, and it declares none. |
| *key field "…" was not supplied*, *nothing to update: no field was set or supplied*, *… requires … — no value was supplied* | The agent left out a value the request needs. |
| *"…" is not a boolean / a whole number / a number / a GUID / a recognised date*, *value is n characters, but the field holds m* | A value does not fit the field's type or length. |

Messages inside a result (the call itself did not fail):

| Code | Meaning |
| - | - |
| `NN/NO_KEY` | A create answered with success but SAP assigned no key, so nothing was created. |
| `NN/DRAFT_NOT_ACTIVATED` | A draft was created or edited but a lifecycle step failed, so nothing was saved. The text says whether the draft was discarded. |
| `NN/DRAFT_STEP` | A lifecycle step failed without SAP saying why; the text names the step. |
| `NN/TRUNCATED` | SAP sent more messages than the limits allow and some were omitted. |

Rejected when you save the tool:

| Message | Fix |
| - | - |
| *delete requires allow\_delete to be set explicitly* | Tick **Allow this agent to delete in SAP**. |
| *entity set "…" does not permit …* | Pick an operation the entity set allows. |
| *"…" is an entity TYPE, not an entity set — use "…"* | Use the entity set name. |
| *required field(s) not set: … — bind a value or expose them to the agent* | Set or expose every field SAP requires. |
| *key field "…" must be set here or left to the agent* | A Read, Update or Delete cannot address a row without its key. |
| *… is a draft node: it is created, changed and deleted through its root entity* | Configure the write on the draft root instead. |
| *top must be between 0 and 500* | Lower the Maximum rows. |
| *\$expand path "…" follows more than 6 navigations* | Remove the deepest related record set. |
| *search is an OData V4 feature and this V2 service answers \$search with 400 — use filters instead* | Turn search off, or filter on fields. |
| *field "…": a range needs a date, timestamp or number*, *a range is the agent's to fill in*, *a range filter applies to a query only* | Use a range only on an agent-filled date, timestamp or number field of a query. |
| *card template is n bytes; the limit is 65536*, *card instructions are n bytes; the limit is 4096* | Shorten the designed card's source, or the design brief. |
| *logo\_url must be an http(s) URL* | Use a full `http://` or `https://` address for the logo, or leave it blank. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.