Propose a mapping for a file
const url = 'https://api.sloose.com/ai/auto-map?connector=con_8f2a&keep=0199a1b2-7c3d-7e4f-8a9b-0c1d2e3f4a5b%3A3';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"columns":["example"],"sampleRows":[],"enabledModules":["example"],"decided":{"additionalProperty":["example"]},"decidedFrom":[{"module":"example","apiName":"example","column":"example","expression":"example"}],"declined":[{"module":"example","apiName":"example","column":"example"}],"nameFound":[{"module":"example","apiName":"example","column":"example"}],"keys":{"additionalProperty":{"field":"example","expression":"example"}},"subformCandidates":[{"child":"example","childLabel":"example","parentLabel":"example","column":"example","field":"example","columns":[{"column":"example","field":"example"}],"counts":{"rowsNaming":1,"rowsBlank":1,"parents":1,"mostRows":1}}],"note":"example","conversation":{"importId":"example","place":{"kind":"source","source":"example"},"question":"example","file":{"digest":"example","reading":"example"},"version":1,"last":{"turnId":"example","waiting":[1],"declined":[1]}},"draft":{"importId":"example","scope":"import","draftId":"example","source":"example","version":1,"file":{"digest":"example","reading":"example"}}}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url 'https://api.sloose.com/ai/auto-map?connector=con_8f2a&keep=0199a1b2-7c3d-7e4f-8a9b-0c1d2e3f4a5b%3A3' \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "columns": [ "example" ], "sampleRows": [], "enabledModules": [ "example" ], "decided": { "additionalProperty": [ "example" ] }, "decidedFrom": [ { "module": "example", "apiName": "example", "column": "example", "expression": "example" } ], "declined": [ { "module": "example", "apiName": "example", "column": "example" } ], "nameFound": [ { "module": "example", "apiName": "example", "column": "example" } ], "keys": { "additionalProperty": { "field": "example", "expression": "example" } }, "subformCandidates": [ { "child": "example", "childLabel": "example", "parentLabel": "example", "column": "example", "field": "example", "columns": [ { "column": "example", "field": "example" } ], "counts": { "rowsNaming": 1, "rowsBlank": 1, "parents": 1, "mostRows": 1 } } ], "note": "example", "conversation": { "importId": "example", "place": { "kind": "source", "source": "example" }, "question": "example", "file": { "digest": "example", "reading": "example" }, "version": 1, "last": { "turnId": "example", "waiting": [ 1 ], "declined": [ 1 ] } }, "draft": { "importId": "example", "scope": "import", "draftId": "example", "source": "example", "version": 1, "file": { "digest": "example", "reading": "example" } } }'Given the file’s columns and a few sample rows, proposes a destination field for each column, with a confidence and a reason.
Every proposal is validated against the organisation’s own fields before it is returned — the model cannot invent a field that does not exist, and a proposal that fails validation is downgraded or dropped rather than handed to you. This is a proposal, not a mutation: nothing is saved.
Mappings only. How an existing record is recognised is a separate question with a separate price: ask POST /ai/match, and pass what it answered back here as keys so the mapping does not propose those fields again. The answer no longer carries matchRules.
What the model sees. Fields the organisation has excluded from AI are withheld from every sample — never sent in any form; their examples stand in for them — from the moment they are excluded: on /ai/chat an earlier turn is replayed as it was first sent — and an organisation whose AI sample policy is deny sends it no rows at all. The same applies to what a failed expression is allowed to say back: a thrown value is a value.
What you see is your own, where it is computed for you. This policy governs the model, not this response. Evidence produced FOR the caller comes from the rows you sent, unmasked, because you are the organisation and the model is not: the sample outputs on a validated proposal, and a formula’s own results and error.
Except what mirrors the conversation. A tool_result event on /ai/chat carries what the MODEL was told, so under masked or deny it carries the model’s copy rather than yours. It is capped at 200 characters and ellipsed past that, so a short result arrives whole and a long one does not — check for the ellipsis rather than assuming either. And none of this says anything about the rest of a response, which may be a generated file or a list of destination options rather than anything of yours.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Query Parameters
Section titled “Query Parameters”The connection this is about, by id: one of the organisation’s, or your own. REQUIRED: absent or empty is 400 CONNECTION_REQUIRED, listing the connections this bearer may name, because nothing picks one for a request that does not say (design §14.8). Any other — another organisation’s, or another person’s — is 404 CONNECTION_NOT_FOUND.
Example
con_8f2aThe connection this is about, by id: one of the organisation’s, or your own. REQUIRED: absent or empty is 400 CONNECTION_REQUIRED, listing the connections this bearer may name, because nothing picks one for a request that does not say (design §14.8). Any other — another organisation’s, or another person’s — is 404 CONNECTION_NOT_FOUND.
The versions of the organisation’s helper libraries the import keeps (“Keep v3 for this import”), which this request evaluates in place of the current ones: comma-separated libraryId:version, one per library, as the resolved configuration names each library (libraryId) — at most as many as an import may keep (200). Each is read from the organisation’s own stored publish — nothing a request sends is run. One that names no publish of a library the organisation holds now is 409 KEPT_VERSION_NOT_FOUND, with missing; one that cannot be read is 400 BAD_INPUT. Absent, every library runs as it currently is.
Example
0199a1b2-7c3d-7e4f-8a9b-0c1d2e3f4a5b:3The versions of the organisation’s helper libraries the import keeps (“Keep v3 for this import”), which this request evaluates in place of the current ones: comma-separated libraryId:version, one per library, as the resolved configuration names each library (libraryId) — at most as many as an import may keep (200). Each is read from the organisation’s own stored publish — nothing a request sends is run. One that names no publish of a library the organisation holds now is 409 KEPT_VERSION_NOT_FOUND, with missing; one that cannot be read is 400 BAD_INPUT. Absent, every library runs as it currently is.
Header Parameters
Section titled “Header Parameters”The organisation you believe this session is in. When it is not the organisation the session is in NOW — POST /auth/session/org moves a session without issuing a new token — the request is refused with 409 ORG_CHANGED rather than served against the other organisation’s schema. Omit it and nothing changes.
The organisation you believe this session is in. When it is not the organisation the session is in NOW — POST /auth/session/org moves a session without issuing a new token — the request is refused with 409 ORG_CHANGED rather than served against the other organisation’s schema. Omit it and nothing changes.
Request Bodyrequired
Section titled “Request Bodyrequired”object
The source file’s column headings, in order.
Sample rows for context. At most 20 are used; the rest are ignored rather than refused. Columns named like a field excluded from AI are withheld (never sent, in any form), and an organisation whose AI sample policy is deny sends it none at all — see the operation description for what that does and does not say about the response you get back.
One source row: column name → cell value, always as a string.
object
Restrict the proposal to these modules. Defaults to everything the organisation has enabled.
Fields already settled, as module key → field apiNames — chosen by a person, or wired by your own defaults. They are named in the prompt so the model spends its effort on what is genuinely open. Never send a mapping your own name matcher guessed: a guess that happened to run first would silence the better answer, which is the thing being paid for.
object
What fills each field in decided, as (module, apiName) with the column it reads as it is or the expression it is filled from. Context only: the prompt says each settled field with what fills it, so a column holding more than that field (a whole name a settled last name is taken from) can still fill another, while a column that is just the field stays closed. It settles nothing: an entry for a field decided does not name is ignored, the first entry for a field is the one said, and a column that is not one of columns or an expression past 2000 characters is left out (the field is still said, with nothing beside it). At most 1000.
object
Proposals this person has already turned down for this file, as (module, apiName, column). The PAIRING is refused and nothing else — the prompt says so, so the field may still be proposed from another column and the column used for another field. At most 200. This changes the answer, not the price: auto-map is banded on the column count either way.
object
What your own name check found, as (module, apiName, column): a strong HINT, never settled — the opposite of decided. The prompt asks the model to propose each of these fields too, confirming the column or improving it with an expression where the values’ shape does not fit the field (a case, a split, a join, a date format, a prefix); a picklist’s column is kept as it is, its values matched at Translate values. At most 200.
object
The key already chosen per module key — the field that recognises an existing record and the expression it is filled from, as POST /ai/match answered it or as a person set it. Context, not a question: the prompt says the field is already mapped, and its column spoken for unless it holds more than the key (a whole name keyed on its surname still gives the first name), so the proposal does not tread on it. Modules outside the import are ignored.
object
object
The field apiName that recognises an existing record.
The expression its value comes from, when known.
The subforms the file may be the rows of (a module whose records live inside another module’s field), as the page worked them out of the file: the subform’s key and label, the parent module’s label, the link column and the parent field its values name, the columns that read as the subform’s fields, and exact counts (rows that name a parent, rows that name none, parents, the most rows to one). The model is told them and may name one in the answer’s subform with a reason; the counts are yours and never the model’s, and a subform it names that is not among these is dropped. A subform named in enabledModules is admitted for the ask. At most 4.
object
object
object
What the person doing the import said before asking — “the address is where we ship to” — for what the file cannot say about itself. Given to the model as context, in their words, never as an instruction that changes what the route answers. At most 500 characters.
A conversation kept with the import, at a module’s Map columns ({ kind: 'cols', source, module }; any other place is 400 PLACE_NOT_ANSWERED): the ask about every column of the file, made from that module’s part. See the operation description.
object
object
object
object
object
object
object
object
object
object
What to record as the question when the request asks none in words (a suggestion pressed, say). Absent, the route records the question the request asks.
The file the asker believes the import holds for the place, as the import last said it: the digest of its bytes (null for one stored before digests were kept) and how they are read — the source’s reader, options and worksheet as one string (fileReading in @sloose/core). The answer is made from the rows the request sends and its turn kept against the file the import holds, so a file replaced, or read again under other options, since the asker last read the import is refused before anything is asked or charged (409 FILE_CHANGED). Absent, the question is about whatever file the import holds.
object
The import’s version the asker’s picture of it was built from: what it last wrote or read, with nothing of its own left unwritten. What became of earlier answers is judged, and a new answer’s was recorded, from the import as the server holds it, so a question asked of a picture the import has moved on from — another person’s edit, or the asker’s own that was never saved — is refused before anything is asked or charged (409 IMPORT_CHANGED, with the version it is at and who changed it last). Absent, the question is about the import as it is.
The kept turn whose answer is live on the page — its newest that proposed anything, which an answer in words alone leaves standing — and which of its targets, by position, still wait on the page (waiting) or were turned down there (declined): what only the page knows. Read only at the formula editor’s place (formula), whose answer waits in the editor and nowhere else, and taken there only when it names the turn the question is judged against: the newest of the turns sent that proposed anything. At Source and every place of Map fields the import keeps its guesses stamped with the answer that made them, so what still waits and what was turned down is read off the stamps each file’s work keeps, and a last sent there is not read.
object
A draft asking — Draft everything, Fill the gaps for what a template’s check left open, or Draft it anyway for one file and module (draft.scope): the answer is kept as a turn at each module’s Map columns, each turn recording which (draft.source names the file). See the operation description.
object
How much of the import this draft was asked about, kept on each part’s turn (ConversationTurn.draftScope): import for Draft everything, gaps for Fill the gaps, anyway for Draft it anyway. Absent, the turn says nothing of it, and reads as the whole import.
Which draft — Draft everything, Fill the gaps or Draft it anyway — this call is part of: the same on every call of one draft, and kept on each part’s turn (ConversationTurn.draftId), so the import’s AI activity lists the draft as one entry. Absent, each turn is an entry of its own.
The file the answer is about: required where the route answers about a file’s modules (/ai/match, /ai/auto-map), and not sent for Translate values’ (/ai/value-map), which are the import’s.
The import’s version the draft’s picture of it was built from, as a kept question’s version (409 IMPORT_CHANGED).
The file the draft believes the import holds, as a kept question’s file (409 FILE_CHANGED).
object
Responses
Section titled “Responses”The proposal. libraryProblems reports any helper library that would not load. With a conversation, turn is the turn it was kept as; with a draft, turns are the turns it was kept as, one per module.
A validated mapping proposal, with what it cost.
object
This call’s id. It is also the ledger’s idempotency key.
Credits settled for this call.
Token counts, including cache reads and writes.
object
The model that answered.
The subform the model says the file is the rows of — only one the request offered in subformCandidates, matched by child and column — with its reason in words. Null when it named none. A choice and a reason, never a count: the figures are the request’s.
object
object
object
object
object
object
object
object
object
object
object
Its position in the conversation at its place, from 0.
The AI route that answered it.
What the answer proposed, and where.
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
What the answer said that is not the answer: a caveat, what the server refused.
What of the file the answer was made from: its values (allow); the distinct values of list fields, a picklist’s or a multi-select’s, as Translate values sends them under Masked (values); their shapes (masked); or none of it (none) — counting the earlier turns it was sent with, since an answer can repeat what one of them said.
When the question was asked, which the history window counts from.
Who asked: a person, or the person whose API token asked. Null once they are deleted.
object
How the question was asked: a person’s session in the app (session), an organisation API token (api_token), or an agent over MCP (mcp).
Whether the next question at its place may send it to the model again. False for a turn before where replay starts, which a stricter AI data setting moves past everything already asked; only the newest 6 are ever sent.
Whether a draft asked it — Draft everything, Fill the gaps or Draft it anyway (draftScope): one call of the draft answers several parts, and each part’s share of the answer is kept at that part’s own place, as a turn saying so, so a later question there — and Go there — finds it (D6). A draft’s turn is sent with no earlier turns, and lands after whatever its place holds without moving where replay starts.
Which draft asked it — Draft everything, Fill the gaps or Draft it anyway: the same for every part’s turn of one draft, so the import’s AI activity lists the draft as one entry. Null for a turn no draft asked, and for a draft’s turn kept before drafts said so, which is an entry of its own.
How much of the import the draft that asked it was asked about: import, Draft everything; gaps, Fill the gaps, the parts a template’s check left open and no other; anyway, Draft it anyway, one file and module a draft found the file does not feed. Null for a turn no draft asked, and for a draft’s turn kept before drafts said, which reads as the whole import.
object
object
object
object
object
object
object
object
object
object
Its position in the conversation at its place, from 0.
The AI route that answered it.
What the answer proposed, and where.
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
object
What the answer said that is not the answer: a caveat, what the server refused.
What of the file the answer was made from: its values (allow); the distinct values of list fields, a picklist’s or a multi-select’s, as Translate values sends them under Masked (values); their shapes (masked); or none of it (none) — counting the earlier turns it was sent with, since an answer can repeat what one of them said.
When the question was asked, which the history window counts from.
Who asked: a person, or the person whose API token asked. Null once they are deleted.
object
How the question was asked: a person’s session in the app (session), an organisation API token (api_token), or an agent over MCP (mcp).
Whether the next question at its place may send it to the model again. False for a turn before where replay starts, which a stricter AI data setting moves past everything already asked; only the newest 6 are ever sent.
Whether a draft asked it — Draft everything, Fill the gaps or Draft it anyway (draftScope): one call of the draft answers several parts, and each part’s share of the answer is kept at that part’s own place, as a turn saying so, so a later question there — and Go there — finds it (D6). A draft’s turn is sent with no earlier turns, and lands after whatever its place holds without moving where replay starts.
Which draft asked it — Draft everything, Fill the gaps or Draft it anyway: the same for every part’s turn of one draft, so the import’s AI activity lists the draft as one entry. Null for a turn no draft asked, and for a draft’s turn kept before drafts said so, which is an entry of its own.
How much of the import the draft that asked it was asked about: import, Draft everything; gaps, Fill the gaps, the parts a template’s check left open and no other; anyway, Draft it anyway, one file and module a draft found the file does not feed. Null for a turn no draft asked, and for a draft’s turn kept before drafts said, which reads as the whole import.
Said once, when the request named a conversation and this question starts afresh because the organisation’s AI data setting is stricter than the one its earlier turns were made from. Null otherwise.
Example
{ "subform": { "why": "" }, "turn": { "place": { "kind": "source" }, "route": "source", "targets": [ { "kind": "filter" } ], "samples": "allow", "askedVia": "session", "draft": false, "draftId": null, "draftScope": "import" }, "turns": [ { "place": { "kind": "source" }, "route": "source", "targets": [ { "kind": "filter" } ], "samples": "allow", "askedVia": "session", "draft": false, "draftId": null, "draftScope": "import" } ]}The body did not match the schema (BAD_INPUT, with issues — conversation and draft together is one); the request named no connection (CONNECTION_REQUIRED, with connections); or keep cannot be read (BAD_INPUT); or conversation names a place this route does not answer, or draft names no file (PLACE_NOT_ANSWERED).
object
Examplegenerated
{ "error": "example", "code": "example"}No bearer, or one that no longer resolves: a session that is unknown or expired (code: AUTH_REQUIRED), or an API token that was revoked or expired (TOKEN_REVOKED, TOKEN_EXPIRED), whose organisation is gone (ORG_NOT_FOUND) or whose member or user row is (MEMBERSHIP_GONE).
The error envelope every non-2xx answer uses.
object
Human-readable explanation.
Machine-readable reason. Absent on a few legacy 400s.
Examplegenerated
{ "error": "example", "code": "example"}Not enough AI credits (CREDITS_EXHAUSTED), or the organisation is suspended (SUSPENDED). balance shows what is left, needed what this asked for, planIncludesCredits whether the plan includes any each period, and renewsAt, where a subscription renews them, when they come back.
object
AI credits. Included credits reset each period; purchased ones do not.
object
Included + purchased − reserved
The least the action would hold: its price, or for an action priced by how much it reads, the cheapest band, since such an action holds what the balance covers of its dearest band. The balance fell short of it — which is not the same as nothing left.
When the credits ran short (CREDITS_EXHAUSTED) under a subscription that renews them — active, not set to cancel, and renewing on a plan with an allowance, whether charged to a card or billed by invoice (whose credits come as each invoice is issued, #1337): when that allowance comes back, the end of the current period. Absent on a plan that includes none, on a trial, under a subscription that is cancelled, set to cancel or past due, when the plan it moves to at the period’s end includes none, for a suspension, and when the plan could not be read.
When the credits ran short (CREDITS_EXHAUSTED): whether the organisation’s plan, as the catalogue has it, includes AI credits each period — a staff override changes this period’s limits, not that. Absent for a suspension, and when the plan could not be read — which says nothing either way.
Examplegenerated
{ "error": "example", "code": "example", "balance": { "included": 1, "purchased": 1, "reserved": 1, "available": 1, "periodStart": "example", "periodEnd": "example" }, "needed": 1, "renewsAt": "example", "planIncludesCredits": true}The import conversation or draft names is one this person may not change: an operator asks about only an import they started (NOT_YOURS). Like every route behind a session: a session signed in but in no organisation is refused code: NO_ORG, a banned person code: BANNED, and a closed organisation code: ORG_CLOSED.
object
Example
{ "code": "NOT_YOURS"}No such organisation (ORG_NOT_FOUND); ?connector= names a connection this person may not use (CONNECTION_NOT_FOUND); the import named is not this organisation’s, or is being deleted (IMPORT_NOT_FOUND); it does not hold the file named (SOURCE_NOT_FOUND); or the import or that file went, or the file was replaced or read again, while the answer was written, so it was not kept and nothing was charged (CONVERSATION_GONE).
object
Example
{ "code": "ORG_NOT_FOUND"}The organisation has no module registry yet (ORG_NOT_CONFIGURED); the session is in a different organisation now (ORG_CHANGED); a version keep names is not one the organisation holds now (KEPT_VERSION_NOT_FOUND, with missing); the import writes to another connection than ?connector= names (CONNECTION_MISMATCH); the file named is not the one the import holds (FILE_CHANGED); the import moved on from the version named (IMPORT_CHANGED, with the version it is at, updatedBy and updatedAt); or another turn landed at the conversation’s place while this one was answered (CONVERSATION_MOVED_ON). None of these is charged.
object
IMPORT_CHANGED only: the version the import is at now.
IMPORT_CHANGED only: who changed it last.
object
The app an agent changed it through over MCP, when its last write was one: userId is then the person whose agent it was.
IMPORT_CHANGED only: when it was last changed.
KEPT_VERSION_NOT_FOUND only: each kept publish keep names that the organisation does not hold now, by its library’s id and number.
object
Example
{ "code": "ORG_NOT_CONFIGURED"}The model declined. category says why.
object
Example
{ "code": "AI_REFUSED"}The model’s answer could not be used.
object
Example
{ "code": "AI_BAD_OUTPUT"}AI is not configured on this deployment (AI_NOT_CONFIGURED): unconfigured paths say so rather than failing. Or the model could not be asked just now (AI_UNAVAILABLE) — the gateway out of credits, rate limited or failing, or the provider unavailable: nothing was changed and nothing was charged, so the same request can be sent again shortly. Or the restore switch is on (code: MAINTENANCE): every route but /health and /robots.txt is refused before anything is read, with Retry-After (the operator’s ADMIN_TOKEN alone is let through to POST /staff/people/{id}/delete, and to nothing else).
Headers
Section titled “Headers”Seconds until asking again is worth it (300 while the switch is on).