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": [[...], ...]}undercontext._tables.<name>, columns named after the projected fields (a leadingdata.in the path is dropped). Rules and transformers see it as a DuckDB tablectx_<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¶
- The session is created with the caller-supplied context (period, fund
number, …) and
context_fetch_report.status = "pending";session.context_fetch_startedfires. - The worker runs the sources sequentially; a failing source is recorded and the others still run.
- 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. - The merged context is re-validated against the schema.
session.context_fetch_completedorsession.context_fetch_failedfires.
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.