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

# Business Central

> Look up, create and update Business Central records, post a verified purchase invoice match, or confirm a purchase order.

The Business Central node connects a workflow to Microsoft Dynamics 365 Business Central. Pick an **Operation**: **Look up** reads a record in the middle of a run and hands it to the steps after it, **Insert** creates a record such as a sales order or purchase invoice, **Update** writes approved values back to an existing record, **Post Purchase Invoice** posts a verified purchase invoice match against its purchase order, and **Confirm Purchase Order** applies an order confirmation match to a purchase order. A following step is required after a lookup and optional after a write: a write can end a branch, or hand the created record to the steps after it.

## Configuration

| Field                    | Type              | Required                      | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ------------------------ | ----------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Connector**            | select            | Yes                           | A [connector](/admin/connectors) of type **Business Central**                                                                                                                                                                                                                                                                                                                                                                                                                |
| **Operation**            | select            | Yes                           | `Insert` (default) creates a new record, `Update` writes changes back to an existing record, `Look up` reads a record (see [Operation](#operation)); `Post Purchase Invoice` posts a verified [purchase invoice](/nodes/reconcile#purchase-invoice) match (see [Post purchase invoice](#post-purchase-invoice)); `Confirm Purchase Order` applies an [order confirmation](/nodes/reconcile#order-confirmation) match (see [Confirm purchase order](#confirm-purchase-order)) |
| **Entity**               | entity picker     | Yes (Insert, Update, Look up) | The Business Central entity set to read from or write to, for example `salesOrders` (see [Entity selection](#entity-selection)). The posting operations inherit their purchase order from the matched result                                                                                                                                                                                                                                                                 |
| **Result**               | select            | Yes (Look up)                 | **Look up** only. `First match` (default) or `All matches` (see [Result](#result))                                                                                                                                                                                                                                                                                                                                                                                           |
| **Rules**                | rule editor       | Yes (Look up)                 | **Look up** only. At least one. Each rule compares a field on the Business Central record against a value, written as the literal type you pick (see [Rules and operators](#rules-and-operators) and [Value types](#value-types))                                                                                                                                                                                                                                            |
| **Select**               | field picker      | No                            | **Look up** only. Field names to return, picked from the connector's catalog or typed. Leave empty to return every field the connector sends                                                                                                                                                                                                                                                                                                                                 |
| **Expand**               | navigation picker | No                            | **Look up** only. Navigation properties to pull in with the record, picked from the catalog or typed, for example `purchaseOrderLines`                                                                                                                                                                                                                                                                                                                                       |
| **Matched invoice**      | select            | Yes (Post Purchase Invoice)   | **Post Purchase Invoice** only. The upstream Reconcile result (or the Review that continues it) on the Purchase invoice profile. Connection, company, invoice number, quantities and review policy are inherited                                                                                                                                                                                                                                                             |
| **Document date**        | template field    | No                            | **Post Purchase Invoice** only. The invoice date, as a field or `YYYY-MM-DD`; leave blank to keep the purchase order's document date                                                                                                                                                                                                                                                                                                                                         |
| **Posting date**         | template field    | No                            | **Post Purchase Invoice** only. Leave blank to use Business Central's default posting date                                                                                                                                                                                                                                                                                                                                                                                   |
| **Matched confirmation** | select            | Yes (Confirm Purchase Order)  | **Confirm Purchase Order** only. The upstream Reconcile result (or the Review that continues it) on the Order confirmation profile. The connector is inherited from the purchase order that Reconcile looked up                                                                                                                                                                                                                                                              |
| **Body format**          | select            | No                            | **Insert** / **Update** only. In **Insert**: `JSON` (default) posts a deep insert, `OData Batch` wraps it as a batch operation, `Binary (base64)` sends raw bytes. **Update** always uses `OData Batch` as the transport, but the body itself is the operations document described below, not raw OData                                                                                                                                                                      |
| **Body**                 | template editor   | No                            | **Insert** / **Update** only. Auto-generated from the entity and operation; hand edits are kept (see [Body template](#body-template))                                                                                                                                                                                                                                                                                                                                        |
| **Duplicate check**      | toggle            | No                            | **Insert** only; not available in Update, and a lookup has nothing to guard. Off by default (see [Duplicate check](#duplicate-check))                                                                                                                                                                                                                                                                                                                                        |
| **Headers**              | key-value editor  | No                            | Custom headers, under **Advanced**                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| **Timeout (seconds)**    | number            | No                            | Request timeout in seconds. A whole number from 1 to 300 for a lookup; leave blank to use the system default (45s)                                                                                                                                                                                                                                                                                                                                                           |

<Note>Transient delivery failures are retried automatically by the platform. Use the node's **retry** field to set the maximum number of attempts, between 1 and 5 (default 3). A posting operation checks its saved operation after an interrupted attempt and stops if Business Central cannot confirm the outcome, so a retry never posts twice.</Note>

<Note>
  At least one rule is required in **Look up**. A lookup with no rules would read the whole table, so the node refuses to save until you add one.
</Note>

## Connector

Pick a [connector](/admin/connectors) of type **Business Central**. The connector supplies the app-only token, environment, and company id, so the node needs no URL or HTTP method: Ingestly builds the request from the connector and the entity you choose.

Pointing this node at a QuickBooks connector fails the step rather than quietly reading from QuickBooks. A connector with no base URL, or with no company configured, fails the step with a message naming what is missing: reconnect it or set its company first.

See the [Business Central guide](/guides/business-central) for how to connect and for advanced request patterns (deep insert, multi-entity `$batch`, attaching the source document).

## Entity selection

The **Entity** field is a searchable picker populated live from the connector's OData `$metadata`. Choose a connector first, then pick from its list of entity sets, for example `salesOrders`, `purchaseInvoices`, or `customers`.

<Note>
  The catalog is read through the connector profile of the environment you are editing in. In a Production workflow the connector's **Production** profile must be connected, and in a Development workflow its **Development** profile. When the catalog cannot load, the schema popover in the editor shows the reason, for example the HTTP status Business Central returned or which profile is not connected.
</Note>

* With the catalog loaded, click the refresh action next to the field to re-fetch it after a schema change on the Business Central side.
* If an entity is not in the list (a custom or extension entity), search for it and choose **Use "\<name>"** to enter it directly.
* If the catalog fails to load (or none has loaded yet, for example before a connector is chosen), the field falls back to a plain text input with no refresh action: type the entity name directly once the field is no longer disabled.

For a **Look up**, the picker keeps the full read catalogue; for a write it offers only the entity sets the connector says are writable.

The entity has to be a plain identifier: letters, digits and underscores, starting with a letter or an underscore. Spaces and slashes around the name are trimmed off before that check, so `/purchaseOrders/` is stored and read as `purchaseOrders`. A dot, a space inside the name, a slash inside the name, a leading digit, a template expression, or anything else is refused when you save. A name that is nothing but slashes or spaces is refused as a missing entity.

That trimming applies to **Entity** only. An entry in **Select** or **Expand** has to be a plain identifier on its own, with no surrounding slashes to strip.

## Operation

### Look up

Reads a record out of Microsoft Dynamics 365 Business Central in the middle of a run and hands it to the steps after it. Use it to resolve something the document only refers to: extract an invoice, find the purchase order it names, then [reconcile](/nodes/reconcile) the two.

#### Result

**First match** (default). Asks Business Central for a single record. The matched record *is* the payload, so a downstream reference reads a field straight off it: `{{FindPO.payload.number}}`. When nothing matched, `payload` is `null`.

**All matches**. Asks for a page of records. The payload is the array itself, so `{{FindPO.payload}}` is what a [Loop](/nodes/loop) or a `$each` iterates, and `{{FindPO.payload[0].number}}` reads the first row. When nothing matched, the payload is an empty array rather than `null`, so a `$each` over it is a no-op instead of a resolution failure.

<Warning>
  **All matches** returns at most 100 records, and there is no paging. A query that matches more than 100 returns the first 100 and reports `matchCount` as 100, so `matchCount` alone cannot tell a query that matched exactly 100 from one that matched thousands. `metadata.businessCentral.truncated` is what tells them apart: it is `true` only when records were left behind. Narrow the rules, or branch on `truncated`, when the answer has to be complete.
</Warning>

#### Rules and operators

A rule has a **Field**, an **Operator**, a **Value** and a **Value Type**. Rules combine with **AND**: a record has to satisfy every rule to match.

* **Field** names a column on the Business Central record, not a path into upstream data. Pick it from the connector's catalog for the chosen entity, or type it. It has to be a plain identifier, and Business Central's OData filter has no dot notation, so `vendor.name` is refused when you save. A template expression in this box is refused too.
* **Value** is the template-bearing half, and that is the inverse of a [Validate](/nodes/validation) rule. `{{extract.payload.po_number}}` in the value box is the normal case.
* **Value Type** says how the value is written into the query. It describes the connector's column, not what you type. See [Value types](#value-types).

Business Central publishes twelve operators:

| Operator                  | Matches records whose field                  |
| ------------------------- | -------------------------------------------- |
| **Equals**                | equals the value                             |
| **Not Equals**            | does not equal the value                     |
| **Greater Than**          | is greater than the value                    |
| **Greater Than or Equal** | is greater than or equal to the value        |
| **Less Than**             | is less than the value                       |
| **Less Than or Equal**    | is less than or equal to the value           |
| **Contains**              | contains the value's text                    |
| **Starts With**           | starts with the value's text                 |
| **Ends With**             | ends with the value's text                   |
| **In List**               | is one of the listed values, comma separated |
| **Is Empty**              | is blank                                     |
| **Is Not Empty**          | holds a value                                |

**Is Empty** and **Is Not Empty** take no value; the value box is hidden for them. Every other operator requires one. Both compare the field against the empty text as well as `null`, because Business Central returns `""` rather than `null` for a text field that holds nothing. On a number, date or GUID column, use **Equals** with the column's blank value instead (`0`, `0001-01-01`, or the all-zero GUID) together with the matching value type.

<Note>
  The [QuickBooks node's Look up operation](/nodes/quickbooks#look-up) publishes only nine of these. **Not Equals**, **Is Empty** and **Is Not Empty** have no equivalent in Intuit's query API, so they are Business Central only. A rule authored here does not necessarily port to a QuickBooks lookup.
</Note>

##### Value types

A rule value is written into the OData query as a literal, and OData literals are typed. A quoted literal is text, so `totalAmountIncludingTax gt '1250.50'` compares a decimal column against a string and Business Central rejects the whole request. **Value Type** is how you say which literal to write:

| Value type         | Written as                             | Use it for                                                                                            |
| ------------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| **Text** (default) | `'PO-1001'`                            | Names, codes, document numbers. Business Central's `number` is text even when it looks like a number. |
| **Number**         | `1250.50`                              | Amounts, quantities, line numbers. Digits, optionally a leading minus and a decimal point.            |
| **Date**           | `2026-09-02`                           | Date-only columns such as `postingDate`. Write it as `YYYY-MM-DD`.                                    |
| **Date and time**  | `2026-09-02T14:30:00Z`                 | Timestamp columns such as `lastModifiedDateTime`. The offset (`Z` or `+02:00`) is required.           |
| **True/false**     | `true`                                 | Boolean columns. `TRUE` and `False` are accepted and written in lower case.                           |
| **GUID**           | `11111111-2222-3333-4444-555555555555` | `id` and `systemId`. Braces, parentheses and the hyphenless form are all accepted.                    |

Ingestly cannot work the type out for you. Business Central's `number` column is text and routinely holds `1001`, so a value of `1001` is a text comparison on one column and a numeric one on another, and only the column says which. Leave the type on **Text** unless you know the column is not.

A value that does not match its type is refused when you save, naming the shape it expected. A value that is a template is not judged at save time (`{{extract.payload.total}}` is not a malformed number, it is not a number yet); if it resolves to something that is not a valid literal of that type, the step fails at run time with the same message.

<Note>
  **Contains**, **Starts With** and **Ends With** have no value type. They compile to OData's text functions, whose argument is always text, so the control is hidden for them and a type set before you switched operators is dropped.

  The [QuickBooks node's Look up operation](/nodes/quickbooks#look-up) has no value types at all. Its query dialect quotes every literal, so there is nothing to choose.
</Note>

##### Values that resolve to nothing

A value box is filled in at save time, but it is resolved at run time, and a template can resolve to nothing. For six operators, an empty resolved value would match every record in the table, so the step fails instead:

* **Contains**, **Starts With**, **Ends With**, **Greater Than**, **Greater Than or Equal** and **Not Equals** refuse an empty resolved value, naming the field and the operator.
* **Equals**, **Less Than** and **Less Than or Equal** are allowed through, because those stay narrow rather than widening.
* **In List** needs at least one non-empty entry after the commas are split.

This is a run-time failure, not a save-time one: a rule using one of those six operators, bound to a field the extract did not produce, saves cleanly and fails when it runs. The other three do not fail. An **Equals**, **Less Than** or **Less Than or Equal** rule whose value resolves to nothing compares the field against an empty string, so the lookup runs and, unless a record's field is itself blank, reports `found` as `false`: a missing extraction reads as "not found", and nothing tells you the question was never asked. Bind the value to a field you know the upstream step emits, and treat `found: false` after an Equals rule as "check the extraction" as well as "check the tenant".

##### Reserved field names

`null`, `true`, `false`, `NaN` and `INF` are literals in OData filter expressions rather than field names, so a rule whose field is exactly one of them is refused when you save. A field called `null` would compile to `null eq null`, which matches every record.

#### Select and Expand

**Select** limits which fields come back. Leave it empty to take everything the connector sends. **Expand** pulls a navigation property in with the record, for example `purchaseOrderLines`, so the lines arrive on the same result instead of needing a second lookup.

Both are lists of plain identifiers: pick each name from the connector's catalog, or type it and choose **Add**. Neither accepts a dotted path: they name top-level fields and navigation properties, not nested ones.

Business Central returns exactly the fields you select and adds nothing. If the record feeds a Business Central node in **Update**, through a [Reconcile](/nodes/reconcile) or directly, keep `id` in the list: the write-back reads the record id from this record and fails without it.

<Note>
  **Expand** is Business Central only. The [QuickBooks node's Look up operation](/nodes/quickbooks#look-up) has no such field, because QuickBooks has no OData surface.
</Note>

#### When nothing matches

**A lookup that matches nothing is a successful step, not an error.** The step completes, `payload` is empty (`null` in **First match**, `[]` in **All matches**), and `metadata.businessCentral.found` is `false`.

That is deliberate. "This purchase order does not exist in Business Central" is a business outcome your workflow should route on, not an outage. Only a query that could not be *asked* fails the step: a bad connector, a rejected query, an HTTP error from Business Central, or a lookup that did not answer within its **Timeout** (45 seconds unless you set one).

Route on it with the node's own ports. In **Look up** the node has no plain output: it leaves through one of two ports instead.

| Port          | Fires when                                                           |
| ------------- | -------------------------------------------------------------------- |
| **Found**     | At least one record matched                                          |
| **Not found** | Nothing matched (in **All matches**, the payload is the empty array) |

With **Fail if not found** off, exactly one of the two fires. Connect the path that uses the record to **Found** and whatever "we could not find it" should do to **Not found**: a [Review](/nodes/review) for a human to resolve, a notification, or nothing at all, since a run that leaves through an unconnected port simply stops there and still completes. At least one of the two has to be connected. With **Fail if not found** on, connect **Found** to the next step; no matches fail the step instead of taking **Not found**.

#### Fail if not found

Enable **Fail if not found** when a matching record is required, such as the purchase order named on an invoice. With it on, no matches fail the step instead of taking the **Not found** branch; leave **Continue on fail** off and the error output unconnected to stop the run. It is off by default, keeping the **Found** / **Not found** branches, applies to both result modes, and leaves connection and API errors on their normal error handling.

<Warning>
  Do not test for a missing record with an [If](/nodes/if) node that reads `{{FindPO.payload.number}}`. A no-match payload is `null`, so a reference through it resolves to nothing, which is the same thing a genuine record with a blank `number` produces. The **Not found** port is the routing answer; `metadata.businessCentral.found` is the field to read when a later step needs the fact inside a template.
</Warning>

### Insert

The default operation. Creates a new record from the current document. The body auto-scaffolds a deep insert: a header object with the parent entity's fields plus a nested line array, built from the columns carried into this step.

### Update

Writes changes back to an existing Business Central record. The body is an **operations document**: a list of operations that each state what to do, not how to send it. Ingestly compiles the list into the connector's own `$batch` request; you never write an OData entity-set path, a key-in-parentheses reference, or an HTTP verb.

#### Record source

Choose where the target record's id, and its line rows, come from.

**Reconcile** (default, recommended). Ingestly builds the write plan from an upstream [Reconcile](/nodes/reconcile) step's verified match, and keeps its safety gate.

| Field                                   | Type            | Required | Description                                                                                                                                                                                  |
| --------------------------------------- | --------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Reconcile node**                      | select          | Yes      | The upstream Reconcile step whose approved values are written back                                                                                                                           |
| **Record id path** (under **Advanced**) | template editor | Yes      | Path to the record's id inside the connector record Reconcile fetched. Defaults to `{{value[0].id}}` for Business Central; override only if this entity's id field differs                   |
| **Line id path** (under **Advanced**)   | template editor | No       | Path to a matched connector line's id, scoped to that single line row, not the whole record. Defaults to `{{id}}` for Business Central; override only if this entity's line id field differs |
| **Line id column** (under **Advanced**) | text            | No       | The output key the matched line's id is written under, so the body can read it back as `{{$item.<name>}}`. Defaults to `lineId`                                                              |

<Note>Line id path and Line id column work together: the server stamps a matched or removed line row's connector line id only when both are set, reading the id from Line id path and writing it under the Line id column key. If the two disagree (for example, the column renamed to `rowId` while the body still reads `{{$item.lineId}}`), the id lands where nothing looks; left blank, line operations resolve their id to nothing. Business Central defaults both fields once a connector and entity are chosen, so leaving them alone is normally correct: open **Advanced** only if this entity's line id field differs from the default.</Note>

<Note>Two safety behaviors apply: if the reconcile result has a blocking mismatch and no approved review, the step fails instead of writing; if nothing changed and there is nothing to send, the step succeeds without sending a request.</Note>

**Direct** (opt-in). No Reconcile node needed. You supply the record id, and optionally the line arrays, directly.

| Field             | Type            | Required                                 | Description                                                                                                       |
| ----------------- | --------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| **Record id**     | template editor | Yes                                      | Any template expression that resolves to the connector record's id, for example `{{extract.payload.orderNumber}}` |
| **Lines**         | template editor | No                                       | An upstream array whose rows already carry a connector line id. Each row becomes a line update                    |
| **Added lines**   | template editor | No                                       | An upstream array of new lines to create. These rows have no line id yet                                          |
| **Removed lines** | template editor | No                                       | An upstream array whose rows already carry a connector line id. Each row becomes a line delete                    |
| **Line id field** | text            | Yes when Lines or Removed lines is bound | The row field that holds the connector line id, for example `lineId`                                              |

<Warning>Direct skips the Reconcile safety gate: whatever record id and line references resolve below are written as-is, with nothing verifying them against the source document first. Switch to Reconcile when that verification matters.</Warning>

For line pairing too complex for a template to express (for example, matching document lines to fetched connector lines by more than one key), add a [Transform](/nodes/transform) step upstream that joins the two sides and emits rows carrying both, then point Direct's **Lines** field at that array. This keeps the pairing logic in one script instead of a second one inside the connector node.

<Note>Direct never fetches the connector record, so `{{$writeback.record.*}}` has nothing to read: a body that references it under Direct is a design-time error. Bind any field you need from the connector record (an ETag, a last-modified timestamp) from whichever upstream node already supplied the record id instead.</Note>

#### The operations document

The auto-generated body is a list of operations under a top-level `operations` array:

| Field          | Required           | Meaning                                                        |
| -------------- | ------------------ | -------------------------------------------------------------- |
| `op`           | Yes                | `create`, `update`, or `delete`                                |
| `entity`       | Yes                | The entity set name, validated against the connector's catalog |
| `id`           | For update/delete  | The record's key; any template expression                      |
| `parentEntity` | With `parentId`    | The owning record's entity set                                 |
| `parentId`     | For child entities | The owning record's key                                        |
| `values`       | For create/update  | Flat field map; values are templates                           |

Business Central exposes line items as addressable child records, so a line operation is its own entry: it carries its own `entity` and `id`, plus `parentEntity` and `parentId` for the parent it belongs to. The two travel together: never one without the other.

```json theme={null}
{
  "operations": [
    {
      "op": "update",
      "entity": "salesOrders",
      "id": "{{$writeback.recordId}}",
      "values": { "postingDate": "{{extract.payload.postingDate}}" }
    },
    {
      "$each": "{{$writeback.updates}}",
      "op": "update",
      "entity": "salesOrderLines",
      "id": "{{$item.lineId}}",
      "parentEntity": "salesOrders",
      "parentId": "{{$writeback.recordId}}",
      "values": { "quantity": "{{$item.quantity}}" }
    },
    {
      "$each": "{{$writeback.adds}}",
      "op": "create",
      "entity": "salesOrderLines",
      "parentEntity": "salesOrders",
      "parentId": "{{$writeback.recordId}}",
      "values": { "quantity": "{{$item.quantity}}" }
    },
    {
      "$each": "{{$writeback.removes}}",
      "op": "delete",
      "entity": "salesOrderLines",
      "id": "{{$item.lineId}}",
      "parentEntity": "salesOrders",
      "parentId": "{{$writeback.recordId}}"
    }
  ]
}
```

`$each` iterates a write-plan array the same way it does in any JSON body: the directive object's own keys are the per-row template (see [row expansion](/guides/expressions#row-expansion-in-json-bodies-each-and-when)). `{{$writeback.recordId}}`, `{{$writeback.updates}}`, `{{$writeback.adds}}`, and `{{$writeback.removes}}` come from the record source configured above; `{{$item.lineId}}` is the connector line id carried on each matched or removed row.

<Tip>Ingestly derives the request from `op` / `entity` / `id` because the connector and entity are already configured on the node, so it can build the navigation path correctly every time. For a request this can't express, use the [HTTP action](/nodes/http-action) node instead.</Tip>

Business Central applies the whole operations document as a single transactional `$batch` request: every operation succeeds, or none do.

<Note>Use `{{$writeback.*}}`, not `{{writeback.*}}`. There is no reserved bare `writeback` head: an unprefixed reference is an ordinary node reference and fails if no node named `writeback` exists upstream. The `$` prefix is what lets a workflow also have a node named `writeback` with no ambiguity.</Note>

### Post purchase invoice

Use this operation after a **Purchase invoice** [Reconcile](/nodes/reconcile#purchase-invoice) whose purchase order source is a Business Central **Look up**. Choose the **Matched invoice** result. The node inherits the purchase order, vendor invoice number, proposed quantities, connection, company and review policy from that match. You do not configure an entity, a body or individual quantity updates.

Leave **Document date** blank to keep the purchase order's document date, or point it at the invoice date field; it sets the Business Central document date and recalculates payment dates. Leave **Posting date** blank to use Business Central's default posting date. When price checks are off in Reconcile, Business Central's own prices apply.

<Note>
  Posting requires the **Ingestly Connector** extension in the selected Business Central environment. In that company, open **Ingestly Connector Setup** and turn on **Enabled** under **Purchase posting**. Enable it in your sandbox first to test your configuration. The capability status beside the matched invoice tells you whether posting is enabled; if it is not, the node stops before writing anything.
</Note>

Assign **Ingestly Connector Setup** (`ING PURCH ADMIN`) to the administrator who manages the setup page, and **Ingestly Purchase Posting** (`ING PURCH POST`) to the Microsoft Entra application behind the connector, together with the usual Business Central permissions to read and post purchases. Scope each assignment to the intended company. Enablement is per company: turn it on in every company you post from, and check it again after the extension is installed or updated. After changing the setup, refresh the capability status in Ingestly.

The operation posts one positive invoice against one purchase order using the receiving mode Reconcile chose: **Receive and invoice** receives what is not yet received and invoices the matched quantity, **Use existing receipts** invoices received quantities only. It accounts for quantities already received and leaves omitted purchase order lines untouched. If the purchase order changed since the match, it rechecks the proposal; with **Review differences** on in Reconcile a new problem opens a review task on the posting step, otherwise the step stops.

#### Configurations the operation refuses

Supported today: **Item**, **G/L Account** and **Item Charge** lines, partial invoices, quantities already received but not yet invoiced, and approved extra lines. Everything below is checked before any write and refuses the whole post rather than posting part of the order. Handle those orders in Business Central.

| Refused                                                               | What triggers it                                                              |
| --------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Warehouse receiving                                                   | A line whose location requires a warehouse receiving process                  |
| Tracked items                                                         | An item that has an item tracking code                                        |
| Drop shipments, special orders, prepayments, projects, blanket orders | The line carries one of those flags or links                                  |
| Pending approval                                                      | A purchase order that has not completed its Business Central approval process |
| Other line types                                                      | Any line that is not **Item**, **G/L Account** or **Item Charge**             |
| Unallocated charges                                                   | An **Item Charge** line not yet allocated to item lines                       |
| Oversized orders                                                      | A purchase order with more than 2,000 lines                                   |
| Quantity precision                                                    | A receive or invoice quantity with more than five decimal places              |
| Duplicate invoices                                                    | A posted purchase invoice already exists for this vendor invoice number       |

Receiving more than was ordered needs an [over-receipt allowance](/nodes/reconcile#over-receipt-allowance) in Reconcile and an **Over-Receipt Code** on the line whose tolerance permits the excess; the smaller of the two limits applies.

**Preview** and **Test Step** do not post. A successful run returns the posted invoice, its receipts, the operation ID and the posting time; receipts are empty when existing receipts covered the invoice. The output also carries the purchase order after posting (`order`) with its status (`open` or `closed`), a link while it stays open and, when **Archive Orders** is on in Purchases & Payables Setup, the archived version (`order.archive`).

If Business Central does not confirm an attempted post, the step reports **Recovery required**. A retry checks the saved operation before posting anything further; it never blindly sends the invoice again. Do not create a second invoice to work around an unresolved posting result.

To build the complete flow, pick **Post a purchase invoice against a purchase order** in the workflow templates. Choose the connection, partial-invoice policy and reviewer, then inspect the preview. Applying the template replaces the canvas in one undoable edit.

### Confirm purchase order

Applies the amendment an [Order confirmation](/nodes/reconcile#order-confirmation) Reconcile prepared: confirmed quantities and prices within tolerance, expected receipt dates and the vendor order number are written to the purchase order in one transaction. The node reads everything from the **Matched confirmation**; the purchase order, its company and the connection come from the lookup that Reconcile compared against.

| Field                    | Description                                                                                                                       |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| **Matched confirmation** | The Reconcile result to apply, or the Review that continues it. Only Order confirmation results are offered                       |
| **Retry attempts**       | How many times a failed attempt is retried. Every attempt reuses the same operation, so a retry never applies the amendment twice |

<Note>
  This operation needs the **Ingestly Connector** extension in Business Central with **Enable purchase order amendments** turned on, and the **Ingestly Purchase Amendment** permission set assigned to the connector's application. The operation selector shows **Purchase order confirmations are supported** once both are in place. See [Connectors](/admin/connectors#business-central-client-credentials).
</Note>

Before writing, Ingestly refreshes the purchase order and checks the match again. If the purchase order changed since the review, or Business Central refuses the change (a quantity below what has been received, a price on an invoiced line), the run opens a new review round instead of writing. A missing extension or permission fails the step.

A confirmation that matched the purchase order exactly still runs the operation and reports **no changes applied**, so the run record shows the confirmation was checked.

A preview or a test run never writes: the step stops before the amendment and reports that confirming a purchase order is unavailable in preview and single-step tests.

## Duplicate check

Applies when **Operation** is `Insert`; the panel is not available in Update, and a lookup has nothing to guard. An Update writes to a record that already exists, so a lost response can't be resolved by looking the record up the same way a lost create can; it fails the step rather than risk a wrong resolution.

<Note>
  Turn on **Duplicate check** to identify the record this step creates by its natural key. Before every write, Ingestly looks that key up in Business Central and fails the step without writing when a matching record already exists, so running the same document twice, or restarting a workflow, cannot create a second record. Leave it off and nothing at the destination is checked.

  * **Entity:** defaults to the node's configured entity.
  * **Keys:** one or more field/value pairs that identify the record by its natural key, for example `Field: externalDocumentNumber`, `Value: {{extract.payload.invoiceNumber}}`.
</Note>

A blocked write fails the step with a message naming the entity, the key values, and the existing record's document number, for example `The destination already has purchaseInvoices with vendorInvoiceNumber I-99576 (number 107378). Nothing was sent.` The run's callback history records it as skipped rather than delivered. If the lookup itself fails, the step fails without sending and is retried under the node's retry policy; it never falls back to a blind write.

The same lookup also covers a lost response: if the connection drops after Business Central accepts the write but before Ingestly sees the answer, the retry finds the record and reuses it instead of creating a second one. Without a duplicate check an unresolved attempt like that fails instead of guessing.

## Body template

The body auto-generates from the entity, operation, and the columns carried into this step, using the same scaffolds as the [Business Central guide](/guides/business-central#2-deep-insert-recommended-for-one-parent--its-children)'s deep insert. Edit the body directly and your changes are preserved: the scaffold only regenerates while the body is still exactly what it last generated, so a hand-edited body is never overwritten. Use the **Generate body** action to reset a body back to a fresh scaffold.

### Advanced

The **Advanced** section exposes the raw body template editor (with autocomplete and validation) and the header editor. JSON bodies support [row expansion with `$each`, `$when`, and `$case`](/guides/expressions#row-expansion-in-json-bodies-each-and-when).

## Inputs and outputs

**Allowed inputs:** any trigger, and any action node that produces a result payload (extract, validation, transform, filter, split, classify, merge, parse, review, wait, if, switch, loop, variable, store, prompt, reconcile, HTTP, call workflow, Business Central, QuickBooks, callback). A Business Central node can feed a Business Central node: write the invoice, then read the record back.

**Output:** An **Insert** or **Update** sends the request to Business Central and makes the response available to later steps. Request-level fields land under `metadata.businessCentral` (`statusCode`, `method`, `url`, `attempts`, `requestSucceeded`). A following step is optional: the node can end a branch, or feed the steps after it.

A **Look up** hands the matched record (or the array of them) down as `payload`, with the outcome under `metadata.businessCentral` (`found`, `matchCount`, `truncated`, `statusCode`). It leaves through its **Found** or **Not found** port rather than a plain output (see [When nothing matches](#when-nothing-matches)), so every following step hangs off one of the two. A lookup costs 0 credits, and a [dry run](/guides/test-mode) does not skip it: the query reaches your live tenant. It reads a record; it does not read the document.

## Using the result downstream

**After an Insert or Update**, the node's response is available to later steps. A JSON write returns the created or updated record, so `{{NodeName.payload.id}}` is the new record's id. An OData `$batch` write instead returns `{{NodeName.payload.responses}}`, one entry per operation, each carrying `id`, `status`, `headers` and `body`, so the first operation's new id is `{{NodeName.payload.responses[0].body.id}}`.

**After a Confirm Purchase Order**, `payload` describes what was written: `purchaseOrderId` and `purchaseOrderNumber`, `changeCount` and `noChanges`, `reopened` (whether a released order was reopened and released again), `purchaseOrderVersionBefore` and `purchaseOrderVersionAfter`, `vendorOrderNumber` (`from` and `to`) when it changed, one entry per changed line under `lines` with `quantity`, `netUnitPrice` and `expectedReceiptDate` changes (each `from` and `to`), and `appliedOn`.

**After a Look up**, `payload` is the connector record itself, with no wrapper around it, so a downstream reference names the Business Central field directly:

| Reference                      | Result mode | Value                                     |
| ------------------------------ | ----------- | ----------------------------------------- |
| `{{FindPO.payload.number}}`    | First match | The matched record's `number` field       |
| `{{FindPO.payload}}`           | All matches | The whole array of matched records        |
| `{{FindPO.payload[0].number}}` | All matches | The first matched record's `number` field |

<Note>
  Ingestly does not know the shape of a Business Central record, so autocomplete stops at `payload`. The fields under it still resolve at run time; they are simply not offered as suggestions. Use **Select** to make it obvious which fields a step is meant to be reading.
</Note>

**`metadata.businessCentral`** carries the request and the outcome:

| Field                | Description                                                                                                                                                          |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **statusCode**       | The HTTP response status code                                                                                                                                        |
| **requestSucceeded** | **Insert** and **Update** only. Whether the request returned a 2xx status                                                                                            |
| **url**              | **Insert** and **Update** only. The URL that was actually called. Emitted on every run but not offered by autocomplete; type it by hand                              |
| **method**           | **Insert** and **Update** only. The HTTP method that was used, in upper case (`POST` for an insert and for a `$batch`), or `NONE` when an Update had nothing to send |
| **attempts**         | **Insert** and **Update** only. The retry attempt number for this request. Emitted but not offered by autocomplete                                                   |
| **found**            | **Look up** only. Whether anything matched                                                                                                                           |
| **matchCount**       | **Look up** only. How many records came back (at most 100 in **All matches**)                                                                                        |
| **truncated**        | **Look up** only. `true` when more records matched than came back. Always `false` in **First match**, which asks for one record by design                            |

An **Update** with nothing to send (the reconcile found no changes) makes no request at all. The step succeeds with a `null` payload, `statusCode` 204 and `method` `NONE`.

<Note>
  The node sits in the **Actions** group of the node picker. In **Insert**, **Update** or **Confirm Purchase Order** it behaves as an output: a following step is optional, it cannot be disabled, and **Test Step** is not offered because a test would write to your live tenant. In **Look up** a following step is required, the node may be disabled, and Test Step runs the real query. Test Step is also refused on any node below an **Insert**, **Update** or **Confirm Purchase Order**, since a test run executes every step above the one being tested.
</Note>

## Error output

Turn on **Error output** in the node's settings to add an **Error** port beside the node's other ports. When the write fails after its retries, or a lookup fails, the run follows that port instead of failing, so you can route the failure to a [Review](/nodes/review), a notification, or a compensating step. With no error edge drawn, a failed step fails the run exactly as before.

A no-match takes this port only when **Fail if not found** is on. With the option off, no matches are a successful lookup and take **Not found**.

<Note>
  Inside a [loop](/nodes/loop) body the **Error output** toggle is not offered and the error port never renders. Whether a failing item stops the loop is the loop's own **If an item fails** setting instead. See [Routing failures](/guides/conditional-routing#routing-failures).
</Note>

## Related

<CardGroup cols={2}>
  <Card title="Business Central guide" icon="building" href="/guides/business-central">
    Connector setup, deep insert, and multi-entity batch requests
  </Card>

  <Card title="Reconcile" icon="scale-balanced" href="/nodes/reconcile">
    Compare the looked-up record against the document
  </Card>

  <Card title="QuickBooks" icon="calculator" href="/nodes/quickbooks">
    The Look up, Insert and Update operations against QuickBooks Online
  </Card>

  <Card title="Connectors" icon="plug" href="/admin/connectors">
    Set up a Business Central connector
  </Card>

  <Card title="Order confirmation" icon="clipboard-check" href="/nodes/reconcile#order-confirmation">
    Prepare the amendment this node applies
  </Card>
</CardGroup>
