使用方法
- 复制安装命令,在自己的智能体环境安装前先检查源码包。
- 从原始文档核对所需工具、凭据和支持的宿主。
- 说明输入材料与期望交付物,并对照来源检查输出。
npx skills add https://github.com/celigo/ai --skill writing-settings-forms开始前需了解
- 安装不包含模型订阅、API 额度或访问文件的权限。
- 仓库星数和安装量反映活跃情况,不等于独立质量或安全测试。
按源仓库内容呈现,保留标题、案例、代码、表格、链接以及原文引用的演示图片。
<!-- TIER:1 -->
Writing Settings Forms
A settings form (settingsForm) is the form the Celigo UI renders to collect a resource's settings: 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 settings, and that's what steps read at run time -- {{settings.flow.region}} in a template, options.settings.flow.region in a script.
Seven places carry a form, each with its own settings: an integration, each of an integration's flow groups (flowGroupings[]), 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
settingsstores the object it's given: a field the form marksrequiredcan 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
formandinitare kept: any other top-level key insettingsFormis dropped without an error. - Not what steps read. Steps read
settings, never the form. A new field gives steps nothing until its value is insettings, 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 (
list_integrations,upsert_integration,upsert_flow,upsert_export, ...) -- references/mcp.md. The deployed server writes forms only through the full-replaceupsert_*tools, and can't run an init script or preview a dropdown's export. - The `celigo` CLI -- references/cli.md
Reading a resource only reads. Writing a form or its settings updates the resource, and a full update replaces the whole document: a body that leaves out settingsForm or settings erases them.
The Seven Scopes
| Scope | Form | Values | Steps read them as |
|---|---|---|---|
| Integration | settingsForm | settings | settings.integration.<name>, in every flow of the integration |
| Flow group | flowGroupings[].settingsForm on the integration | flowGroupings[].settings | settings.flowGrouping.<name>, in the flows of that group |
| Flow | settingsForm | settings | settings.flow.<name>, in the flow's steps |
| Export (lookups too) | settingsForm | settings | settings.export.<name>, in that step only |
| Import | settingsForm | settings | settings.import.<name>, in that step only |
| Connection | settingsForm | settings | settings.connection.<name>, in steps using the connection |
| iClient | settingsForm | settings | settings.iClient.<name>, in steps whose connection uses it |
The scope key is literal: {{settings.region}} (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 settings.flow.region, and each nested key adds a segment. Templates, filters, and Mapper 2.0 read these paths (writing-handlebars > Context Decision Matrix); scripts read options.settings.<scope> (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
{
"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, id, name | Use one string for all three. name is the key the value is saved under, directly in settings; id is how the layout and conditions refer to the field. Keep it a plain key with no dots: the UI saves a.b as nested objects but reads it back from a flat a.b key, so the field reloads empty |
type | The control -- see Field Types |
label, description, helpText | The label beside the field, a line of text under it, and the help icon's text |
required | Blocks the UI's Save while the field is empty (default false); API writes skip it |
defaultValue | What the UI shows while settings holds no value under the field's name |
options | The choices of a select, multiselect, or radiogroup: groups ([{"items": [...]}], each item a string or a {label, value} pair) or bare {label, value} entries. A toggle takes bare {label, value} entries. The saved value is the item's value |
| Conditions | visibleWhen and its relatives -- see Conditions |
omitWhenHidden | true 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
type | Control | Saved value |
|---|---|---|
text | Text input; multiline: true for a text area, inputType (number, email, password, tel, url) for a typed input, delimiter to split the input into an array | String (array with delimiter) |
checkbox | Checkbox | Boolean |
toggle | A switch between the entries of options | The chosen entry's value |
select, radiogroup | Dropdown, radio buttons | The chosen item's value |
multiselect | Dropdown allowing several choices | Array of values |
date, datetime | Date or date-and-time picker; doNotAllowFutureDates, skipTimezoneConversion | The picked date |
relativeuri | Text input for a relative URI | String |
editor | Code editor; mode is json, xml, csv, or text | The editor's content |
keyvalue | Rows of two free-text inputs; keyName / valueName name them (default key / value), showDelete adds a delete button per row | Array of {<keyName>, <valueName>} rows |
staticMap | Two-column mapping table whose columns can offer choices | Array of {<keyName>, <valueName>} rows -- see Dynamic Fields |
exportSelect | Dropdown whose options an export returns | The chosen option's value (an array with multiselect: true) -- see Dynamic Fields |
Conditions
A condition rule is {"field": "<field id>", "is": [<values>]} (or "isNot"). Two set values compare as strings, so "is": ["true"] and "is": [true] both match a checked checkbox; match an unchecked one with [false].
visibleWhenshows the field while any rule matches;visibleWhenAllwhile all of them do. With neither, the field always shows.requiredWhen/requiredWhenAllanddisabledWhen/disabledWhenAllmake the field required or read-only the same way.- A rule can hold nested
"AND": [rules]or"OR": [rules]in place offield. - A rule naming a field the form doesn't have counts as matching for
visibleWhen(the field always shows) and as not matching forrequiredWhenanddisabledWhen-- the save doesn't catch it.
Layout
form.layout arranges fields; a field the layout doesn't list isn't shown, and a layout id with no fieldMap entry renders nothing.
- A layout has a
type,containers, and optionallyfields(ids shown before the containers). - The layout's
typesets how its containers render:columnside by side,collapseas collapsible sections (closed until opened;"collapsed": falseopens one),boxas bordered groups,indentas indented groups,tabWithoutSaveandverticalTabWithoutSaveas tabs. - Each container has a
label(its heading) andfields, and can carry its owntypeandcontainersto nest another level, as the Advanced container does in The Shape.
Init Scripts
settingsForm.init -- {"_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 settingsForm.form, then fills in the values from settings; a throw shows its message in place of the form.
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:
options.resource(the resource whose form it is -- for a flow group, the group:_id,name,settings,settingsForm),options.sandbox, and on an integration and a flow group the parents: a flow group gets its integration asparentResource(and a parent integration asgrandparentResource), a child integration gets its parent asparentResource.options.licenseandoptions.parentLicenseappear only in Integration Apps. Flows, exports, imports, and connections get onlyresourceandsandbox. - The script must be the account's own. Any other
_scriptIdis 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
initshows an error instead of the form -- and an iClient save stores any_scriptIdwithout checking it. - Testing: no CLI command or MCP tool calls the init route. Run the function through the JavaScript processor with an
optionsobject you build -- see How to Check a Form.
Schema Index
The form's full shape is the Form schema on the API reference's Integrations page (agent-readable markdown; exports and imports embed the same schema), and settings 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 --
options.settingsin hooks, and the init function point - configuring-filters > Gotchas -- settings scopes in filter operands
- configuring-exports -- the export body an
exportSelectruns - 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 resource.virtual:
"fieldMap": {
"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" }
]
}
}
}
}
}
},
"carrier": { "id": "carrier", "name": "carrier", "type": "select", "label": "Carrier" }
}The exportSelect entry nests six levels deep; check that the next field (carrier here) still sits in fieldMap, beside it -- one closing brace too many puts it under form, outside fieldMap, and a layout id with no fieldMap entry renders nothing.
- The export is an export body, not a reference. There's no way to point at a saved export by id: put
adaptorType, its adaptor block, and an optionaltransforminvirtual. Withoutresource.virtualthe field shows "requires export resource."_connectionIddefaults 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
data-- the records after the transform, output filter, and preSavePage hook. Each record needs avalueand alabel; a bare string or number serves as both. The preview reads at most 100 records: the UI sendstest.limit100 unless the export sets its own, which can't exceed 100. An HTTP export that finds no records answers without adatalist, so the field shows "expecting array. try transform?" rather than an empty list -- as it does for any answer that isn't a list. - `multiselect: true` allows several choices, saved as an array.
- `supportHandlebars: true` renders the export body as a template before each preview (everything but
transform,hooks, andfilter). The template's context isn't the runtime's: the form's current values sit under the form's own scope name (integration,flowGrouping,flow,export, orimport), 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 nosettings.prefix:{{integration.accountId}},{{export.country}}. Connection and iClient forms get an empty context. A templated list doesn't load on its own when the form opens: it loads throughrefreshOptionsOnChangesToortypeAheadSearch, 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
supportHandlebars. - `typeAheadSearch: true` 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 name as an array of rows: [{"source": "pending", "target": "open"}].
- `keyName` / `valueName` name the columns -- the keys of each saved row. `keyLabel` / `valueLabel` head them.
- `keyOptions` / `valueOptions` give a column a suggestion list:
{label, value}items ({id, text}and[value, label]work too). The cell still accepts typed values. - `keyResource` / `valueResource` --
{"virtual": {<export body>}}-- fill a column's suggestions from an inline export, previewed as forexportSelect(no templating). With either one set, both columns are required unless the field setskeyValueRequired: false, 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#eachover 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 settingsForm to extend, the current settings, and everything else a full update must send back. On an integration, a flow group's form and values live in its flowGroupings[] entry.
3. Write the fields
One fieldMap entry per setting, with the same string as key, id, and name -- a plain key, no dots. Choose the type (Field Types), give options to choice fields, and a defaultValue where the UI should suggest one. For a list of values the user picks from another system, use an exportSelect; for a value-to-value table, a staticMap (step 6).
4. Lay the fields out
List every field in the layout exactly once. Choose the top-level type, group related fields into labeled containers, and nest a container's own type and containers for a second level.
5. Add conditions
Show, require, or lock fields with visibleWhen / requiredWhen / disabledWhen (or their *All forms), naming fields by id. Add omitWhenHidden: true to a field whose value shouldn't be saved while it's hidden.
6. Build the dynamic fields
Write the inline export of each exportSelect (or a staticMap column's keyResource / valueResource) and preview it before you save the form: it must return label / value records -- shape them with a transform. Add supportHandlebars 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 settingsForm.init. The returned form replaces settingsForm.form in the UI, so keep form as the fallback the function starts from.
8. Save the form and the values together
Send the form and the matching settings values in one update -- the full document, read in step 2 -- so steps have values before anyone opens the form. The form you send replaces the stored one, so to change one field start from the stored settingsForm and keep every other fieldMap entry and layout id. Before sending, check the whole body rather than the field you changed: fieldMap and layout sit side by side directly in form, and every id in layout is a key directly in fieldMap. Keep every value steps read in the form: a save in the UI writes settings 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 $.settings.flow.region 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
settingsFormandsettingswith what you sent. A save never validates the form, so check it yourself: every layout id has afieldMapentry, every field is in the layout once, every condition names an existing field, key =id=nameon each field, and choice fields haveoptions. - Preview each inline export an
exportSelectorstaticMapruns, with"test": {"limit": 100}added the way the UI sends it -- a preview without it returns one record. Itsdatamust be a list oflabel/valuerecords. - Run the init function through the JavaScript processor, with the
optionsobject it will receive (resource,sandbox, and the parents its scope gets). The reply'sdatais 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
settingsin its data ({"record": {...}, "settings": {"flow": {"region": "eu"}}}), or preview the step withsettingsin 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
- [ ] The form sits on the narrowest scope every reading step shares
- [ ] Each field's
fieldMapkey,id, andnameare the same plain string - [ ] Every field appears in the layout once, and every layout id and condition names an existing field
- [ ] Choice fields have
options; eachexportSelecthasresource.virtualwhose preview returnslabel/valuerecords - [ ] An
init._scriptIdnames a script in this account, and the function was tested; not on an iClient - [ ]
settingsholds a value for every field steps read, written in the same update as the form - [ ] The update sent the complete document:
settingsForm,settings, and everything else the resource had - [ ] Steps reference each value through its scope key
Gotchas
- A full update erases what it omits. A body without
settingsFormorsettingsremoves them, and a body whosesettingsFormhas noinitremoves the init script. Read the whole resource first, and change onlysettingsForm.formwhen that's all you mean to change. - Saves aren't checked against the form. A missing
requiredvalue, extra keys, wrong types, and dangling layout ids or conditions all save; only the UI enforces the form. - Only `form` and `init` are kept. Anything else at the top of
settingsFormis dropped silently. - Values sit directly under `settings`, keyed by `name`. A dotted
namesaves as nested objects but reloads empty, and a value under any other key is invisible to the field. - Steps need the scope key.
{{settings.region}}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
settings.integration,settings.flowGrouping, andsettings.flowempty -- run it as a step of the flow, or the flow itself, to see them. - A hidden field is still saved unless it sets
omitWhenHidden. - A condition on a missing field fails open for visibility.
visibleWhennaming a field that isn't there leaves the field always visible;requiredWhenanddisabledWhennever apply. - The init script replaces the form. The UI shows what the function returns, not
settingsForm.form; a throw replaces the form with the error message. - iClient forms can't use init, and an iClient save accepts any
_scriptIdunchecked. - An exportSelect lists the preview's `data`, at most 100 records. Records without
labelandvaluedon'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
settingsfrom 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
settings(settings.general,settings.sections), 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" | init._scriptId names a script this account doesn't have | Use the _id 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 init) | Run the function through the JavaScript processor to see the error; remove init from an iClient |
| "requires export resource." under an exportSelect | No resource.virtual | Add the inline export body |
| "expecting array. try transform?" | The exportSelect's export found no records (an HTTP preview then has no data) or returned something other than a list | Preview the export with test.limit 100; shape records into label / value with a transform |
| An exportSelect's options are blank or can't be chosen | The records have no label / value | Map them in the export's transform |
A field always shows despite visibleWhen | The rule names a field id that isn't in fieldMap | Name the controlling field's id |
| A field reloads empty after saving | Its name contains a dot, or differs from the key steps read | Use one plain string for key, id, and name |
{{settings.<scope>.<name>}} renders empty | No scope key, the wrong scope, the value isn't in settings, or the step ran outside its flow | Check the path against The Seven Scopes and read settings back |
A whole form or settings disappeared | A full update left it out | Write it back from your copy or the resource's audit log (an update's fieldChanges keep the old values), and send complete documents |

