Skip to main content
This document covers llm_match, the semantic entity-matching tool exposed through the Agents Platform. It matches free text (a vendor name from an invoice, a PO line-item description) against a catalog synced from a connected system (SAP, NetSuite, Postgres, SQL Server, …) via the internal LLM matching service.

Supported integrations

llm_match can only bind to an integration whose service has a registered match adapter. Today that is: Binding the tool to any other integration is rejected with LLM matching not supported for <service>. Two SQL Server credential fields are mapped when the config is handed to the matching service: trust_server_certificate is sent as trust_cert, and host_name_in_certificate is dropped — the matching service has no counterpart for it. A SQL Server whose certificate only validates via that field may connect from mssql_execute_query but fail to sync here.

Authentication and enablement

Configured tool: requires an integration binding (integration_id), a table (table_name), and the column to match on (embedding_key). Optionally bind unique_key_column — the catalog column holding each product’s unique identity (e.g. KUNNR, MATNR, or a derived concat column built on the data side for composite keys). When bound, it is surfaced on results as unique_key_column, always survives return_columns projection, and downstream review/learnings use it as the correction key. The bound integration’s catalog is synced into the matching service before the tool becomes usable (status must be ready; a still-syncing config returns a retriable error). Off by default — enable it in the agent’s Tools panel. The prompt itself and the candidate pool are configurable per tool — see Prompt and retrieval tuning.

Inputs

Required (one of):
  • search_text — a single text to match.
  • search_texts — batch mode: multiple texts matched in one call, one result row per text in input order. Prefer this over calling the tool once per line item (N line items stay one step). When set, search_text is ignored.
Optional:
  • max_candidates (default 1) — how many close matches to return per input. Set 3–5 when the result feeds a human review step so alternatives render as a dropdown.
  • return_columns — project matched rows (and candidate products) to these columns. Use when catalog rows are wide: an untrimmed batch result can exceed the tool-output cap and truncate. The matched column is always retained.
  • filters — structured $and/$or filter over catalog attributes, applied to every input (batch included). See Filters.

Filters

A filter scopes the catalog the matcher searches: the root is a logical operator ($and or $or) whose value is an array of conditions {field, op, value|values}. It applies to every input of the call, batch included, and travels per request — no re-sync.
A condition may also be a nested group — its own $and/$or array — which the matching service evaluates recursively, so status = ACTIVE AND (region = EU OR region IN (US, CA)) is expressible:
Groups nest up to 5 levels; deeper is rejected. Only the flat form is declared on the LLM-facing schema (constrained decoding cannot emit a nested condition), so groups are an admin/binding feature — the model itself only ever produces one level. In the tool setup form the field renders as a builder: one row per condition (column · operator · value), an all-of / any-of selector per level, and “Add group” for nesting. The LLM can still fill the field itself when it is left on “let your agent generate a value”. The filters field as condition rows “Add group” nests the other combinator inside the current one: A nested any-of group inside the all-of root An “Edit as JSON” escape hatch sits underneath, and a filter the tree cannot state truthfully — an operator outside the table below, a group mixing nested logic with field conditions, nesting past the cap — opens directly in JSON, with the reason and no way back to the rows, so nothing is silently rewritten: The JSON fallback for a filter the builder cannot represent One consequence of grouping: review-form dropdowns inherit the filter the matcher ran under only when it is a conjunction. Nested all-of groups are flattened and inherited as usual, but an any-of group anywhere in the tree stamps nothing — a chip row cannot state a disjunction, and half of one would claim a narrowing the matcher never performed — so those dropdowns offer the unfiltered catalog. Every comparison runs on product ->> '<column>', so values match the stored text exactly (false, not No). An invalid filter fails the call fast with a message naming the condition rather than silently matching everything.

Prompt and retrieval tuning (configured tool only)

Five knobs shape the prompt the matching service sends and the candidate pool it picks from. All are admin-bound (x-exclude-from-llm) — the model can neither read nor set them — and all are optional: leave one blank and the matching service keeps its own default. They travel per request, so a change takes effect on the next call with no re-sync.

The prompt

The service substitutes three placeholders and sends whatever is left:
  • {{searchText}} — the text being matched. Required.
  • {{productList}} — the retrieved candidates, one per line, rendered with product_list_template. Required.
  • {{previousMappings}} — learned mappings from past reviewer corrections, rendered with previous_mappings_template. Optional; omit it to ignore learnings for this tool.
A prompt missing either required placeholder is rejected before the call — upstream it would only surface as an opaque “no product identifier found in response”. The prompt must instruct the model to return only the Database Identifier of the best match (one per line when max_candidates > 1); the identifiers are extracted from the reply and anything else is discarded. The assembled prompt is truncated at 300,000 characters by the service.

The row templates

Both take the matching service’s TemplateConfig shape — a fmt format string plus the ordered keys filling its verbs, one rendering per row:
  • One verb per key, in order. Use %s for text and %v where the column may be numeric (JSON numbers rendered with %s come out as %!s(float64=…)). %% is a literal percent.
  • Resolved key names: embedding_key (the matched column’s value), database_identifier (the product’s identity), search_text and match_type (learned mappings only).
  • Any other key is read as a catalog column and renders blank when that column is absent — a typo yields empty fields, not an error.
  • A verb/key count mismatch is rejected up front; upstream it would silently bake %!s(MISSING) into every candidate line.
  • The binding UI stores the JSON editor’s content as a string; both the string and a real object form are accepted.
Widening product_list_template is the usual fix for a match that needs a disambiguator the embedding column doesn’t carry (two vendors with the same name in different cities). Raising similar_products_limit is the fix for the right record existing in the catalog but never reaching the prompt — at the cost of tokens and latency on every match.

num_batches caveat

num_batches is honoured on the single-input (search_text) path only. The matching service accepts it on the async endpoint the search_texts batch path uses, but never persists it on the job row, so a batch call always runs unbatched — the tool logs a warning (num_batches is ignored on batch (search_texts) calls) rather than letting the binding look effective.

Output

Structured content is the shared match-result envelope (same shape as LLM csv_lookup):
  • values / candidate product hold the bare catalog columns (products-master wrappers and embedding vectors are stripped).
  • match_identifier is the matched product’s opaque unique identity — carry it verbatim; never parse or reconstruct it.
  • Single-input calls additionally keep the legacy flat keys (field_match, product_data, …) for pre-envelope consumers.
  • In batch mode a failed or unmatched input still produces a row (empty values, error set) so rows stay positionally aligned with inputs.
The result registers a task variable (${VAR_N}) holding {match_type, config_kind, match_config_id, matches}. A review_form field or table column referencing $VAR.matches... with "editor": "select" auto-renders the matched column as a dropdown of the candidates. Reviewer corrections on those dropdowns (picks or typed values) are submitted back to the matching service as match learnings — asynchronously, after the review is saved — so repeated corrections teach the matcher.

Limits

  • max_candidates ≤ 10.
  • Batch requests fan out to the matching service with bounded concurrency (10) and a 30s per-input polling timeout; the call errors only when every input fails.
  • Not-ready configs self-heal inline for up to ~12s, then return a retriable “still syncing” error.

Side effects

None on the connected system — read-only matching against the synced catalog.

Expected errors

  • search_text (or a non-empty search_texts batch) is required — no usable input.
  • No match configuration found… — the integration/table/field binding has no synced config.
  • Match data is still syncing (status: …) — retriable; the sync hasn’t completed.
  • Match configuration sync failed: … — terminal; re-trigger the sync from tool settings.
  • all N LLM matching requests failed: … — batch mode, every input errored.
  • custom_prompt_template is missing required placeholder(s) … — the bound prompt has no {{searchText}} and/or {{productList}}.
  • product_list_template has N format verb(s) but M key(s) / … is missing a non-empty "template" — malformed row template binding.