<!-- TIER:1 -->
Writing Settings Forms
A settings form (
) is the form the Celigo UI renders to collect a resource's
: which fields appear, their labels and choices, how they're grouped, and which show when. The values it collects are stored in the same resource's
, and that's what steps read at run time --
in a template,
options.settings.flow.region
in a script.
Seven places carry a form, each with its own
: an integration, each of an integration's flow groups (
), a flow, an export, an import, a connection, and an iClient. An Integration App's settings are a different mechanism, driven by the app's own code -- see
managing-integrations > Integration Apps.
What a form is not:
- Not validation. Only the UI renders the form and applies its rules. An API, CLI, or MCP write of stores the object it's given: a field the form marks can be missing, keys the form doesn't define are kept, and nothing checks types or choices.
- Not checked on save. A layout entry or a condition naming a field the form doesn't have is stored as sent. Only and are kept: any other top-level key in is dropped without an error.
- Not what steps read. Steps read , never the form. A new field gives steps nothing until its value is in , so write the value along with the field.
How to Run the Steps
This skill names each step by what it reads or changes; the exact calls depend on how you reach the account. Read the one reference that matches your tools -- both follow the step numbers of How to Build a Settings Form, then How to Check a Form:
- Celigo MCP tools (, , , , ...) -- references/mcp.md. The deployed server writes forms only through the full-replace tools, and can't run an init script or preview a dropdown's export.
- The CLI -- references/cli.md
Reading a resource only reads. Writing a form or its
updates the resource, and a full update replaces the whole document: a body that leaves out
or
erases them.
The Seven Scopes
| Scope | Form | Values | Steps read them as |
|---|
| Integration | | | settings.integration.<name>
, in every flow of the integration |
| Flow group | flowGroupings[].settingsForm
on the integration | | settings.flowGrouping.<name>
, in the flows of that group |
| Flow | | | , in the flow's steps |
| Export (lookups too) | | | , in that step only |
| Import | | | , in that step only |
| Connection | | | settings.connection.<name>
, in steps using the connection |
| iClient | | | , in steps whose connection uses it |
The scope key is literal:
(no scope) and paths built from field labels or section names render empty. After the scope, the path follows the stored JSON -- a top-level value is
, and each nested key adds a segment. Templates, filters, and Mapper 2.0 read these paths (
writing-handlebars > Context Decision Matrix); scripts read
(
writing-scripts > Common Options).
Which scopes a step sees depends on how it runs:
- A flow run gives the whole chain: integration, flow group, flow, connection, iClient, and the step's own scope.
- A step inside an API or a Tool gets only its own scope plus connection and iClient; integration, flow group, and flow resolve empty there.
- A step run on its own -- an export or import invoke with no parent -- gets only its own scope. An import invoke run as a step of a flow sees the flow's chain.
Pick the narrowest scope that every step reading the value shares: a whole integration, a flow group for flows that share a region or subsidiary, a flow, or a step's own form for a knob only that step uses.
Quick Reference
The Shape
json
{
"settingsForm": {
"form": {
"fieldMap": {
"region": {
"id": "region", "name": "region", "type": "select", "label": "Region", "required": true,
"options": [{ "items": [{ "label": "United States", "value": "us" }, { "label": "Europe", "value": "eu" }] }]
},
"vatNumber": {
"id": "vatNumber", "name": "vatNumber", "type": "text", "label": "VAT number",
"visibleWhen": [{ "field": "region", "is": ["eu"] }]
},
"statusMap": {
"id": "statusMap", "name": "statusMap", "type": "staticMap", "label": "Order status map",
"keyName": "source", "keyLabel": "Source status", "valueName": "target", "valueLabel": "Destination status",
"valueOptions": [{ "label": "Open", "value": "open" }, { "label": "Closed", "value": "closed" }]
}
},
"layout": {
"type": "collapse",
"containers": [
{ "label": "General", "fields": ["region", "vatNumber"] },
{ "label": "Advanced", "type": "box", "containers": [{ "label": "Status mapping", "fields": ["statusMap"] }] }
]
}
}
},
"settings": { "region": "eu", "vatNumber": "EU123", "statusMap": [{ "source": "pending", "target": "open" }] }
}
Field Properties
| Property | Rules |
|---|
| fieldMap key, , | Use one string for all three. is the key the value is saved under, directly in ; is how the layout and conditions refer to the field. Keep it a plain key with no dots: the UI saves as nested objects but reads it back from a flat key, so the field reloads empty |
| The control -- see Field Types |
| , , | The label beside the field, a line of text under it, and the help icon's text |
| Blocks the UI's Save while the field is empty (default ); API writes skip it |
| What the UI shows while holds no value under the field's |
| The choices of a , , or : groups (, each item a string or a pair) or bare entries. A takes bare entries. The saved value is the item's |
| Conditions | and its relatives -- see Conditions |
| leaves the field's value out of a save while it's hidden; without it, a hidden field's value is saved like any other |
Field Types
| Control | Saved value |
|---|
| Text input; for a text area, (, , , , ) for a typed input, to split the input into an array | String (array with ) |
| Checkbox | Boolean |
| A switch between the entries of | The chosen entry's |
| , | Dropdown, radio buttons | The chosen item's |
| Dropdown allowing several choices | Array of s |
| , | Date or date-and-time picker; , | The picked date |
| Text input for a relative URI | String |
| Code editor; is , , , or | The editor's content |
| Rows of two free-text inputs; / name them (default / ), adds a delete button per row | Array of rows |
| Two-column mapping table whose columns can offer choices | Array of rows -- see Dynamic Fields |
| Dropdown whose options an export returns | The chosen option's (an array with ) -- see Dynamic Fields |
Conditions
A condition rule is
{"field": "<field id>", "is": [<values>]}
(or
). Two set values compare as strings, so
and
both match a checked checkbox; match an unchecked one with
.
- shows the field while any rule matches; while all of them do. With neither, the field always shows.
- / and / make the field required or read-only the same way.
- A rule can hold nested or in place of .
- A rule naming a field the form doesn't have counts as matching for (the field always shows) and as not matching for and -- the save doesn't catch it.
Layout
arranges fields; a field the layout doesn't list isn't shown, and a layout id with no
entry renders nothing.
- A layout has a , , and optionally (ids shown before the containers).
- The layout's sets how its containers render: side by side, as collapsible sections (closed until opened; opens one), as bordered groups, as indented groups, and as tabs.
- Each container has a (its heading) and , and can carry its own and to nest another level, as the Advanced container does in The Shape.
Init Scripts
--
{"_scriptId": "<scriptId>", "function": "<name>"}
-- runs a script function each time the UI opens the form. The UI renders the form the function returns
instead of , then fills in the values from
; a throw shows its message in place of the form.
javascript
function initForm(options) {
var form = options.resource.settingsForm.form;
// add, remove, or change fields from the resource's own state
return form;
}
- What the function gets: (the resource whose form it is -- for a flow group, the group: , , , ), , and on an integration and a flow group the parents: a flow group gets its integration as (and a parent integration as ), a child integration gets its parent as . and appear only in Integration Apps. Flows, exports, imports, and connections get only and .
- The script must be the account's own. Any other is refused with 422 "Only a script owned by the user or a script shared by the Integration App can be used at settingsForm.init._scriptId" (on a flow group the path is
flowGroupings.<index>.settingsForm.init._scriptId
).
- Not on iClients. The platform's form-init route serves integrations, flow groups, flows, exports, imports, and connections only, so an iClient form with an shows an error instead of the form -- and an iClient save stores any without checking it.
- Testing: no CLI command or MCP tool calls the init route. Run the function through the JavaScript processor with an object you build -- see How to Check a Form.
Schema Index
The form's full shape is the
schema on the API reference's
Integrations page (agent-readable markdown; exports and imports embed the same schema), and
is a free-form object on every scope. Over MCP,
get_schema({ target: "resource", name: "common/form" })
returns it.
Related Skills
- writing-handlebars > Context Decision Matrix -- reading settings values in templates
- writing-scripts > Common Options -- in hooks, and the init function point
- configuring-filters > Gotchas -- settings scopes in filter operands
- configuring-exports -- the export body an runs
- managing-integrations > Organize the resources inside -- creating flow groups and putting flows in them
- managing-integrations > Integration Apps -- app settings, which the app's own code saves
- configuring-connections -- full updates of connections and iClients, which carry masked secrets
<!-- TIER:2 -->
Dynamic Fields: exportSelect and staticMap
exportSelect
A dropdown whose options come from an export the form carries inline, in
:
json
"warehouse": {
"id": "warehouse", "name": "warehouse", "type": "exportSelect", "label": "Warehouse",
"resource": { "virtual": {
"_connectionId": "<connectionId>",
"adaptorType": "HTTPExport",
"http": { "relativeURI": "/v1/warehouses", "method": "GET", "formType": "http", "response": { "resourcePath": "warehouses" } },
"transform": { "type": "expression", "expression": { "version": "2", "rulesTwoDotZero": { "mode": "create", "mappings": [
{ "generate": "label", "extract": "$.name", "dataType": "string", "status": "Active" },
{ "generate": "value", "extract": "$.code", "dataType": "string", "status": "Active" } ] } } }
} }
}
- The export is an export body, not a reference. There's no way to point at a saved export by id: put , its adaptor block, and an optional in . Without the field shows "requires export resource." defaults to the connection of the export or import the form is on; on any other scope, set it.
- The UI previews the export when the form opens (and on Refresh) and lists the preview's -- the records after the transform, output filter, and preSavePage hook. Each record needs a and a ; a bare string or number serves as both. The preview reads at most 100 records: the UI sends 100 unless the export sets its own, which can't exceed 100. An HTTP export that finds no records answers without a list, so the field shows "expecting array. try transform?" rather than an empty list -- as it does for any answer that isn't a list.
- allows several choices, saved as an array.
- renders the export body as a template before each preview (everything but , , and ). The template's context isn't the runtime's: the form's current values sit under the form's own scope name (, , , , or ), beside the saved settings of the scopes above it and, on an export or import form, the whole document of the connection the export uses -- with no prefix:
{{integration.accountId}}
, . Connection and iClient forms get an empty context. A templated list doesn't load on its own when the form opens: it loads through refreshOptionsOnChangesTo
or , or when the user clicks Refresh.
refreshOptionsOnChangesTo: ["<field id>", ...]
loads the templated list once one of those fields has a value and reloads it whenever one changes -- a cascading dropdown. It works only with .
- reloads it as the user types (300 ms after the last keystroke), with the typed text as this field's own value in the context; an empty box clears the list, and free text is accepted.
staticMap
A two-column table, saved under the field's
as an array of rows:
[{"source": "pending", "target": "open"}]
.
- / name the columns -- the keys of each saved row. / head them.
- / give a column a suggestion list: items ( and work too). The cell still accepts typed values.
- / --
{"virtual": {<export body>}}
-- fill a column's suggestions from an inline export, previewed as for (no templating). With either one set, both columns are required unless the field sets , and the UI drops incomplete rows when it saves.
- Steps read the whole array:
options.settings.flow.statusMap.find(r => r.source === status)
in a script, or over it in a template.
How to Build a Settings Form
1. Decide the scope
Who reads the value? Put the form where every reader shares it (
The Seven Scopes), and remember that steps in an API or a Tool see only their own scope plus connection and iClient. For a flow group, create the group first and put the flows in it (
managing-integrations > Organize the resources inside).
2. Read the resource
Read the whole resource: an existing
to extend, the current
, and everything else a full update must send back. On an integration, a flow group's form and values live in its
entry.
3. Write the fields
One
entry per setting, with the same string as key,
, and
-- a plain key, no dots. Choose the
(Field Types), give
to choice fields, and a
where the UI should suggest one. For a list of values the user picks from another system, use an
; for a value-to-value table, a
(step 6).
4. Lay the fields out
List every field in the layout exactly once. Choose the top-level
, group related fields into labeled containers, and nest a container's own
and
for a second level.
5. Add conditions
Show, require, or lock fields with
/
/
(or their
forms), naming fields by id. Add
to a field whose value shouldn't be saved while it's hidden.
6. Build the dynamic fields
Write the inline export of each
(or a
column's
/
) and preview it before you save the form: it must return
/
records -- shape them with a
. Add
and
refreshOptionsOnChangesTo
for a dropdown that depends on another field.
7. Add an init script (optional)
When the form must change with the resource's state, write an init function that returns the form, test it (How to Check a Form), save the script, and set
. The returned form replaces
in the UI, so keep
as the fallback the function starts from.
8. Save the form and the values together
Send the form and the matching
values in one update -- the full document, read in step 2 -- so steps have values before anyone opens the form. Keep every value steps read in the form: a save in the UI writes
from the form's fields.
9. Read the values in steps
Reference each value through its scope key:
{{settings.flowGrouping.region}}
in a template or filter operand,
options.settings.export.batchLimit
in a script, or
in a Mapper 2.0 extract with the envelope input context (
writing-mappings > Choose the input context).
How to Check a Form
- Read the resource back and compare and with what you sent. A save never validates the form, so check it yourself: every layout id has a entry, every field is in the layout once, every condition names an existing field, key = = on each field, and choice fields have .
- Preview each inline export an or runs, with added the way the UI sends it -- a preview without it returns one record. Its must be a list of / records.
- Run the init function through the JavaScript processor, with the object it will receive (, , and the parents its scope gets). The reply's is the form it returns; a throw comes back as 422 with the message.
- Render a template that reads the values with the Handlebars processor, with in its data (
{"record": {...}, "settings": {"flow": {"region": "eu"}}}
), or preview the step with in the document. An import invoke writes to the destination; run one only against a test destination.
- Open the form in the UI. Only the UI renders it -- the layout, the conditions, the dropdowns, and the init script's result.
<!-- TIER:3 -->
Pre-Submit Checklist
Gotchas
- A full update erases what it omits. A body without or removes them, and a body whose has no removes the init script. Read the whole resource first, and change only when that's all you mean to change.
- Saves aren't checked against the form. A missing value, extra keys, wrong types, and dangling layout ids or conditions all save; only the UI enforces the form.
- Only and are kept. Anything else at the top of is dropped silently.
- Values sit directly under , keyed by . A dotted saves as nested objects but reloads empty, and a value under any other key is invisible to the field.
- Steps need the scope key. renders empty; so does a path built from a field's label or a section heading.
- A step run on its own sees only its own scope. An export or import invoke without a parent renders , , and empty -- run it as a step of the flow, or the flow itself, to see them.
- A hidden field is still saved unless it sets .
- A condition on a missing field fails open for visibility. naming a field that isn't there leaves the field always visible; and never apply.
- The init script replaces the form. The UI shows what the function returns, not ; a throw replaces the form with the error message.
- iClient forms can't use init, and an iClient save accepts any unchecked.
- An exportSelect lists the preview's , at most 100 records. Records without and don't make usable options, and an HTTP export that finds nothing shows an error instead of an empty list.
- Keep every value steps read in the form. A UI save writes from the form's fields, so a key you wrote over the API that the form doesn't define won't survive someone saving the form.
- Integration App settings are separate. An app's settings form is in its (, ), and changes go through the app's own save route -- see managing-integrations > Integration Apps.
Common Errors
| Error | Cause | Fix |
|---|
| 422 "Only a script owned by the user or a script shared by the Integration App can be used at settingsForm.init._scriptId" | names a script this account doesn't have | Use the of one of the account's scripts |
| The form shows an error message instead of fields | The init function threw (or the form is on an iClient with an ) | Run the function through the JavaScript processor to see the error; remove from an iClient |
| "requires export resource." under an exportSelect | No | Add the inline export body |
| "expecting array. try transform?" | The exportSelect's export found no records (an HTTP preview then has no ) or returned something other than a list | Preview the export with 100; shape records into / with a |
| An exportSelect's options are blank or can't be chosen | The records have no / | Map them in the export's |
| A field always shows despite | The rule names a field id that isn't in | Name the controlling field's id |
| A field reloads empty after saving | Its contains a dot, or differs from the key steps read | Use one plain string for key, , and |
{{settings.<scope>.<name>}}
renders empty | No scope key, the wrong scope, the value isn't in , or the step ran outside its flow | Check the path against The Seven Scopes and read back |
| A whole form or disappeared | A full update left it out | Write it back from your copy or the resource's audit log (an update's keep the old values), and send complete documents |