Module references
Conduit
Conduit is the data module — capability area data, labelled Conduit (data) in the capability catalog. A workspace registers scoped, read-only connectors (PostgreSQL, MySQL, Snowflake) and a set of named queries: human-authored, single-statement SQL templates with typed parameters. Callers pick a query by slug and supply parameter values; they never supply SQL.
Every execution is checked against a read-only statement policy, capped by the connector's maxRows and timeoutMs, and recorded as an append-only data_calls row. The id of that row comes back as dataCallId — the anchor a [Dn] citation points at. Separately, a query can be bound to a graph entity or entity type, which is what lets other modules resolve "the current numbers for this thing" without anyone writing SQL at answer time.
Use it standalone
Conduit's public surface is three HTTP operations and one React component. Adopting it needs no documents, no knowledge graph, no embeddings, and no chat model.
Issue a key with the data scopes
data:readlists connectors and named queries.data:runexecutes one. They are independent — a read-only integration never needsdata:run. On a delegated user key (eli_uk_) the owner must additionally still hold the matching capability:data:readfor the reads,data:executefor the run. Workspace keys (eli_sk_) are scope-gated only.Install and import one entry point
Headless:
@eli-ai/client/data. React:@eli-ai/react/dataplus@eli-ai/react/provider. Neither pulls in the aggregate client, the query types, or any other module's contracts.Run a query by slug
Parameters are a flat object keyed by declared parameter name. Unknown keys are ignored and never reach SQL.
What this does NOT require
kb:read / kb:write scope, no ingested documents, no entities or ontology, no agent definition or run, no embedding model, and no chat model. The run endpoint performs zero LLM calls — it is SQL execution behind a policy gate. Entity bindings are the one Conduit feature that genuinely needs another module; see How it composes.Reading queries reveals their SQL
GET /api/v1/data/queries returns each query's full sqlTemplate. Treat data:read as "may read the SQL of every named query in the workspace," not merely "may see a list of titles." Connector credentials are never returned by any route — the ciphertext column is stripped and replaced with the boolean hasCredential.React components
@eli-ai/react/data exports exactly one component, EliLiveData, plus its two label/prop types (EliLiveDataProps, EliLiveDataLabels). There is no separate connector-management or binding-editor component in the package.
EliLiveData
A single surface that loads the workspace's connectors and named queries on mount, renders the connectors as badges (the package's positive tone when enabled, neutral otherwise), and renders a form with a query select, a JSON textarea for parameters, and a submit button. Options for disabled queries render but are non-selectable, and each option is labelled title (falling back to slug) followed by its connector name when that connector is in the list. On success it renders a results table with the returned columns as headers, plus badges for rowCount, durationMs, and a "Truncated" badge when the row cap was hit. Cells that are objects or arrays are JSON-stringified; null and undefined render as an em dash.
The component issues one data:read call per list on mount and one data:run call per submit. The parameter textarea starts at {} and must parse to a JSON object: invalid JSON reports "Parameters are not valid JSON." and a JSON array or null reports "Parameters must be a JSON object." — both inline, before any request is made.
EliLiveData props
Do not ship a workspace key to the browser
EliProvider accepts a transport (or an aggregate client whose transport it retains) and never a credential. Give it a fetch that calls your own same-origin endpoint, and attach the eli_sk_ key there. A data:run key in a bundle is a direct execute grant against production databases.HTTP API
Three operations, all under /api/v1/data. Failures use the shared envelope { "error", "message" }; a body that fails validation adds an issues array. Because the apex 308-redirects, pass -L on the non-GET call so curl replays the POST body.
/api/v1/data/connectorsscope data:read · capability data:readEvery data connector in the workspace, ordered by name, credential-masked. Read-only — no connection is opened and nothing is executed.
config is the non-secret connection shape for that kind: Postgres and MySQL carry host · port · database · user (plus sslMode or ssl); Snowflake carries account · database · warehouse · user (plus optional schema and role). schemaSnapshot, when present, holds the discovered tables with their columns (name · dataType · nullable) and a nullable rowEstimate.
/api/v1/data/queriesscope data:read · capability data:readThe workspace's governed named queries, ordered by slug, optionally narrowed to one connector. Nothing is executed here.
Query parameters
Each entry in params declares name, a type of string | number | boolean | date, and optionally required, default, and description. Order is meaningful: the executor binds params[0] to $1, and so on.
/api/v1/data/queries/{slug}/runBearer · data:runExecute one governed named query. The path segment accepts the query's slug or its id. Read-only SQL, row and timeout caps, and a data_calls provenance row are enforced by the executor, below the route.
Request body
truncated is true when the connector's maxRows cap clipped the result — the rows you received are a prefix, not the whole answer. Data-layer failures map their code to a status:
- 404
query-not-found— no query with that slug or id;connector-not-found— the query's connector row is gone. - 409
connector-disabled— emitted when either the query or its connector hasenabled: false. Themessagenames which one. - 400
invalid-params— a missing required param or a value that will not coerce to its declared type;not-read-only— the stored template failed the statement gate. - 502
execution-failedand 504timeout— the external engine errored or exceededtimeoutMs. - 401
unauthorized; 403insufficient_scopeorinsufficient_capability; 400invalid_json/invalid_bodyfrom the shared gate.
Authoring is not part of the v1 key surface
/api/v1 route that creates, edits, or deletes a connector, a named query, or an entity binding, and none that lists bindings. Those are session-authenticated workspace operations under /api/w/…, gated on the data:write capability and owner/admin. An API key can read the catalog and execute a query — nothing more.Read-only is enforced statically, not by trusting the author
SELECT or WITH, with no INSERT, UPDATE, DELETE, INTO, DDL, or session keywords anywhere in it — which is what rejects a writing CTE and SELECT … INTO. Connectors also expose an allowRawSql opt-in for exploratory SQL through other surfaces; it is off by default and is not reachable through the three routes above.Data elements
Four tables. All four are workspace-scoped and carry a row-level-security policy keyed on workspace_id for every operation, so a query outside the workspace transaction context returns nothing — the API's scope check and RLS are two independent gates.
| Table | Conduit | Columns that matter to a consumer | What one row means |
|---|---|---|---|
| data_connectors | owns · RLS | id · workspace_id · name · kind · config · credential_enc · max_rows · timeout_ms · allow_raw_sql · enabled · schema_snapshot · last_status | One scoped external source. credential_enc is AES-256-GCM at rest, decrypted only inside the executor at call time, and never leaves the server — the API returns hasCredential instead. max_rows/timeout_ms are the caps applied to every call. Unique on (workspace_id, name). |
| data_queries | owns · RLS | id · workspace_id · connector_id · slug · title · description · sql_template · params · enabled | One named query. sql_template uses positional $1..$n placeholders only; params is the typed spec array those placeholders bind to, in order. description is written for a model to read. Unique on (workspace_id, slug). |
| entity_bindings | owns · RLS · not on /api/v1 | id · workspace_id · entity_id · entity_type · query_id · label · param_map · enabled | Attaches a named query to one entity or to a whole entity type — exactly one of entity_id / entity_type is set. param_map says where each query param comes from: entity_name, a const value, or model (supplied at call time). Read and written by other modules; no v1 route exposes it. |
| data_calls | writes · RLS · append-only | id · workspace_id · connector_id · query_id · binding_id · run_id · conversation_id · sql_text · params_used · row_count · duration_ms · status · error · executed_by · executed_at | One execution attempt. status is ok | error | timeout | blocked — failures and policy rejections are recorded too, not just successes. sql_text is the exact statement executed. The id is the dataCallId returned by the run endpoint and the target of a [Dn] citation. |
Provenance is written even when the call fails
data_calls row with the corresponding status. That means the audit trail answers "what did we try to ask, and what happened" — not only "what succeeded." The row is append-only; there is no update path.How it composes
Conduit runs entirely on its own for the slug-and-params use case. What the other modules add is binding — the ability to go from a thing in the graph to the live numbers about it without a caller knowing which query to run.
With Atlas — bindings become available
entity_bindings resolves against entity rows: a binding matches either the entity's id or its type, and a lookup for one entity returns its direct bindings plus the ones inherited from its type. Without Atlas there are no entities to bind to, so the binding table stays empty and the param_map source entity_name has nothing to resolve. Everything in the three v1 routes still works unchanged — those never touch bindings.
With Lens — live rows join the cited answer
POST /api/v1/query takes an includeData boolean. When true, the pipeline resolves bindings for the entities it detected in the question, executes those whose params fully resolve without a model source, and returns them as dataCalls — each entry carrying id · dataCallId · connectorName · queryTitle · rowCount · executedAt. It is capped at three data calls per answer, a per-binding failure never blocks the document-grounded answer, and that route gates on agents:run and needs a configured chat model — both things Conduit alone does not. See Lens.
With Warrant — data evidence can overturn an abstain
In the same pipeline, a retrieval-floor verdict of abstain_with_pointers is overridden to proceed when live data actually ran, and the override is recorded on the decision. That is a Warrant policy behaviour reacting to Conduit evidence; it does not exist if you use Conduit by itself.
With Ports — two MCP tools
The MCP server exposes kb_data_discover (accepts data:read or data:run) to list the bindings that apply to a named entity along with the params the caller still has to supply, and kb_data_lookup (requires data:run) to execute one, selected by bindingId or bindingLabel. Omitting both works only when exactly one binding applies. Both mirror the REST scope map rather than bypassing it, and the lookup returns the same eight fields as the run endpoint. See Ports.
With Lineage — the [Dn] trail
data_calls is the provenance substrate a Lineage trace follows for a live figure, the way a document chunk backs an [Sn] citation. Conduit writes those rows unconditionally, so the trail exists whether or not you have adopted the module that reads it.
The honest standalone boundary
Extending Conduit
dataAdapters — resolved synchronously and shape-checked before the executor will use it, with the read-only SQL gate, parameter coercion and row caps staying host-owned. Plugins can also execute governed queries through the facade: ctx.data.conduit.runNamedQuery(slug, params) requires data:execute and appends the same data_calls provenance row any other caller would, so a plugin-driven figure is as citable as a human-driven one. Conduit emits data.call.completed and data.query.saved. See the extension model for the contract and the trust boundary, and Build a plugin for a worked example.