Skip to content

BuiltAPI

Pull master data and history from a BuiltAPI workspace into the session context — the previous month's balances for a reconciliation rule, a fund's master record, a list of valid cost centres — so the submitter only has to name the period and the fund, and Expecto fetches the rest.

Direction: context only. Expecto queries the workspace at session creation and merges the result into the session context as scalars or tables that transformers, entity rules, and business rules can read.

1. Create the connection

Settings → Connections → Add Connection, type BuiltAPI:

Field Value
Base URL Root URL of the BuiltAPI gateway, https://… (a public host — the SSRF guard rejects private ranges)
Workspace ID The workspace UUID; sent as x-builtapi-workspace-id on every call
Credentials A service token with read access to the workspace; sent as Authorization: Bearer …

Test calls the workspace's entity list with take: 1 — the cheapest call that exercises both the token and the workspace membership. query_error here means the workspace id is missing from the config.

POST /api/integration-connections
{
  "name": "BuiltAPI – master data",
  "type": "builtapi",
  "base_url": "https://api.builtapi.example",
  "config": { "workspace_id": "5c1d…" },
  "credentials": "<service token>"
}

2. Declare context sources on the project

A project holds up to 20 context sources. Each one names a connection, a query, and what it produces — exactly one context table or one scalar. Manage them under Project → Context, via PUT /api/projects/{id}/context-sources, or the set_project_context_sources MCP tool:

{
  "sources": [
    {
      "name": "prev_balances",
      "connection_id": "<connection uuid>",
      "query": {
        "kind": "records_search",
        "entity_id": "balances",
        "filter": { "fund": "{{ context.fund_number }}", "period": "{{ context.prev_period }}" },
        "projection": { "account": 1, "closing_balance": 1 }
      },
      "produces": { "table": "prev_balance" }
    },
    {
      "name": "fund_name",
      "connection_id": "<connection uuid>",
      "query": {
        "kind": "records_search",
        "entity_id": "funds",
        "filter": { "number": "{{ context.fund_number }}" },
        "projection": { "name": 1 },
        "take": 1
      },
      "produces": { "scalar": "fund_name" }
    }
  ]
}

PUT replaces the whole list. Source names follow ^[A-Za-z][A-Za-z0-9_]{0,63}$; produced targets must be unique across the project's sources.

Query kinds

records_search — filter + projection over one entity:

Key Meaning
entity_id BuiltAPI entity id (letters, digits, dashes). Never templated.
filter Filter object in BuiltAPI's query syntax. String values may carry placeholders.
projection Field → 1 (include) or 0 (exclude). Produced table columns follow the included fields; a scalar source must project exactly one field.
take Optional total row limit, 1–100 000.

aggregation — a Mongo-style pipeline over one entity, for joins and group-bys:

Key Meaning
entity_id As above.
pipeline Non-empty list of $-stages, e.g. $match, $lookup, $group, $project. Write and server-side-code stages ($out, $merge, $function, …) are rejected at save time.
take Optional total row limit.

Pagination and limits

Expecto pages through the result automatically, 1 000 rows per request, until the entity is exhausted or take is reached. Without take, a result larger than 100 000 rows fails with an explicit error rather than being truncated silently — narrow the filter or set take. Each page may be at most 10 MiB; each request has a 15 s timeout.

What a source produces

  • Table — {"columns": [...], "rows": [[...], ...]} under context._tables.<name>, columns named after the projected fields (a leading data. in the path is dropped). Rules and transformers see it as a DuckDB table ctx_<name>.
  • Scalar — the single projected field of the first record, under context.<name>.

3. Wire it into the context schema

Declare every produced scalar or table in the project's context schema and mark it filled_by: "integration" so submitters are not asked to provide it and the pre-flight validation knows the value comes from the fetch:

{
  "scalars": [
    { "name": "fund_number", "type": "string", "required": true, "filled_by": "org" },
    { "name": "prev_period", "type": "string", "required": true, "filled_by": "org" },
    { "name": "fund_name", "type": "string", "required": true, "filled_by": "integration" }
  ],
  "tables": [
    { "name": "prev_balance", "required": true, "filled_by": "integration", "columns": [...] }
  ]
}

The required flag also decides what an empty result means: for a required target an empty search is an empty_result error that fails the fetch; for an optional one it yields an empty table.

4. Test before you rely on it

Test query on the source card (or POST /api/integration-connections/test-source, MCP test_context_source) runs the query with sample placeholder values and returns the row count, the columns, and the first rows — or the semantic error — without creating a session:

{
  "connection_id": "<uuid>",
  "query": { "kind": "records_search", "entity_id": "funds", "filter": { "number": "{{ context.fund_number }}" }, "projection": { "name": 1 } },
  "produces": { "scalar": "fund_name" },
  "params": { "context.fund_number": "F-1001" }
}

5. What happens at session creation

  1. The session is created with the caller-supplied context (period, fund number, …) and context_fetch_report.status = "pending"; session.context_fetch_started fires.
  2. The worker runs the sources sequentially; a failing source is recorded and the others still run.
  3. Fragments are merged into the session context. A fetched value wins over a caller-supplied one; the report flags such sources with overwrote_caller_value: true.
  4. The merged context is re-validated against the schema. session.context_fetch_completed or session.context_fetch_failed fires.

While the report is pending or failed, the pipeline does not start: the parse job is blocked with per-source, human-readable reasons. Fix the cause (connection, credentials, filter) and re-run the fetch with POST /api/sessions/{id}/context/fetch or the fetch_session_context MCP tool — allowed until the session is confirmed. Manual context edits made in the meantime are kept; fetched keys are refreshed.

The report on the session (GET /api/sessions/{id} → context_fetch_report):

{
  "status": "failed",
  "fetched_at": "2026-09-15T06:12:41Z",
  "sources": [
    { "name": "prev_balances", "status": "ok", "rows": 412, "overwrote_caller_value": false },
    { "name": "fund_name", "status": "error", "error_kind": "empty_result",
      "message": "No value found in BuiltAPI for scalar source 'fund_name'" }
  ]
}

error_kind is one of connection_error, auth_error, query_error, empty_result — see the reference.

Placeholders

Only two namespaces are allowed in a query: {{ context.<scalar> }} for scalars declared in the project's context schema and {{ session.id }}, {{ session.period }}, {{ session.data_provider_id }}. A string that is exactly one placeholder keeps the value's type (numbers stay numbers); placeholders embedded in longer strings render as text. Placeholders are substituted into the JSON body only, never into the URL. Details in the placeholder reference.