<!-- TIER:1 -- Building Tools A tool is Celigo's first-class reusable building block . It encapsulates logic -- lookups, imports, transforms, branching -- behind a defined input and output contract. Build it once, use it everywhere: from Flows, APIs, AI Agents, MCP Servers, and other Tools. Tool Concepts Why tools exist: Without tools, users build the same lookup-transform-import patterns repeatedly across Flows, APIs, and Agents. Tools solve this by providing a governed, composable abstraction: one definition, many consumers, consistent behavior. When to build a tool: - You're building an MC…

— the record root. An object record is addressed as\n '$.field'; a grouped (array) record as '$[0].field' / '$[*].field'\n - Used for precisely targeting data in structured JSON objects\n - Examples: '$.customer.firstName', '$.items[0].price', '$.addresses[*].street',\n '$[*].Tax' (every row of a grouped record)\n - Wildcards like [*] extract multiple values/objects\n - A key that contains a dot needs bracket notation: `$['P.O. Number']` (or\n `$[\"P.O. Number\"]`, `$.[P.O. Number]`). `$.P.O. Number` is read as a nested\n path: it maps nothing and the record still succeeds. Spaces, hyphens and `#`\n need no brackets (`$.Ship To Name`, `$.Tracking #`)\n - Filter expressions such as `$.items[?(@.active)]` fail the record with\n \"Eval [?(expr)] prevented in JSONPath expression.\"\n\n ```json\n \"extract\": \"$.customer.addresses[*]\" // Extracts all addresses\n ```\n\n **2. Handlebars Template Syntax**\n - Contains '{{' and '}}' pattern\n - Evaluated by the AFE 2.0 handlebars template engine\n - Can include logic, formatting, and computation\n - Access input record fields with {{record.fieldName}} notation; a grouped\n (array) record binds as `rows` instead — {{rows.0.fieldName}} / {{#each rows}}\n - Examples: \"{{record.firstName}} {{record.lastName}}\", \"{{#if record.isActive}}Active{{else}}Inactive{{/if}}\"\n - Valid on scalars and on `object` rows, where the rendered output must\n parse into an object. On an array row the template goes in a\n `buildArrayHelper` entry instead: on an `objectarray` row's own\n `extract` it is refused with 422 (\"buildArrayHelper\" or \"hardCodedValue\"\n must be present for object array datatype.), and on a primitive-array\n row's own `extract` it yields nothing (the field is left out) without an\n error\n\n ```json\n \"extract\": \"{{record.price}} {{record.currency}}\" // Combines two fields\n ```\n\n **3. Hard-Coded Value (literal string)**\n - Does NOT start with '

<!-- TIER:1 -- Building Tools A tool is Celigo's first-class reusable building block . It encapsulates logic -- lookups, imports, transforms, branching -- behind a defined input and output contract. Build it once, use it everywhere: from Flows, APIs, AI Agents, MCP Servers, and other Tools. Tool Concepts Why tools exist: Without tools, users build the same lookup-transform-import patterns repeatedly across Flows, APIs, and Agents. Tools solve this by providing a governed, composable abstraction: one definition, many consumers, consistent behavior. When to build a tool: - You're building an MC…

\n - Does NOT contain handlebars '{{' syntax\n - System treats it as a literal string value\n - VERY COMMON for setting static/constant values\n - Examples: \"Active\", \"USD\", \"Completed\", \"true\"\n\n ```json\n \"extract\": \"primary\" // Sets field value to the literal string \"primary\"\n \"extract\": \"true\" // Sets field value to the literal string \"true\"\n \"extract\": \"N/A\" // Sets field value to the literal string \"N/A\"\n ```\n\n This third pattern is the simplest and most efficient way to set hard-coded values in your mappings.\n AI agents should use this pattern whenever a field needs a static value that doesn't come from\n the input record or require computation.\n\n **Important implementation details**\n\n - JSON path patterns ALWAYS execute from the TOP-LEVEL root of the input record\n - The system maintains this context even in deeply nested mappings\n - For object mappings without child mappings, extract should return a complete object\n - An `object` row takes `extract` (to copy a whole object) or child `mappings`, not\n both: a row with both fails with 422 (`\"extract\" should not be present if\n \"mappings\" field is present`). A `buildArrayHelper` entry is different: there\n `extract` and `mappings` work together.\n - The root is the record as it arrives: an object record roots at `$.field`; a\n grouped record (an array of rows — grouped exports, file key columns, NetSuite\n grouped saved searches) roots at the array, so its rows are addressed as\n `$[0].field` (one row) or `$[*].field` (every row). A root that contradicts the\n record's shape resolves to nothing without an error.\n\n For most simple field-to-field mappings, prefer JSON path syntax for its clarity and performance.\n For hard-coded values, simply use the literal string as the extract value.\n x-celigo-ai-guidance:\n - |-\n ## GROUPED (ARRAY) RECORDS\n Decide the JSONPath root from the record's shape before writing\n any extract. When the record is an array of rows, `$.field`,\n `$.properties.field` and `$.rows[*].field` all resolve to\n nothing — the processor still reports success and the request\n body ships as `{}`. Address rows as `$[0].field` (that row's\n value, typed) or `$[*].field` (every row: joined with commas\n into a `string` destination; rejected with\n `incompatible_destination_conversion` into a `number`\n destination). Column labels with spaces work as\n `$[*][Order Number]`, `$[*].[Order Number]`, or\n `$[*]['Order Number']`. For one destination element per row\n the two array kinds take different helper forms: a primitive\n array (`stringarray` / `numberarray`) uses `buildArrayHelper:\n [{\"extract\": \"$[*].field\"}]`; an `objectarray` uses\n `buildArrayHelper: [{\"extract\": \"$[*]\", \"mappings\": [...]}]`\n with the children addressing the row relatively (`$.field`,\n `{{rows.field}}`). An `extract` placed directly on the\n array-typed field yields nothing. Handlebars\n extracts bind the array record as `rows` (`{{rows.0.field}}`),\n an object record as `record`. The reverse holds too: `$[*]` on\n an object record iterates the object's values, not rows.\n - |-\n ## SIZE LIMIT\n The platform's import save validator caps this field at **1024\n characters**. Saves with a longer `extract` are rejected\n with `[size_limit_exceeded] The maximum size of the field:\n mappings.\u003cindex>.extract in the Import document should be\n 1024 characters`. This typically only matters for\n Handlebars templates that build complex strings inline -- if\n a template is approaching the cap, prefer one of:\n\n - **Decompose into nested mappings** (set `dataType:\n \"object\"` and use child `mappings` -- each child gets\n its own 1024-char budget).\n - **Move the logic to a `preMap` hook** so the heavy\n transformation runs in script and the mapping just reads\n `record.\u003ccomputed>`.\n - **Use a lookup** (`lookupName` + a small `lookups` entry)\n for value translation that would otherwise need a long\n `{{#if ...}}{{else if ...}}` chain.\n examples: [\"$.fullName\", \"{{record.fullName}}\", \"primary\", \"$.lineItems[*].unitPrice\"]\n extractDateFormat:\n type: string\n description: |\n Specifies the format pattern of the input date string to ensure proper parsing.\n\n Used on string-typed mappings whose `extract` yields a date. Uses\n Moment.js-compatible formatting tokens to describe how the incoming date\n string is structured.\n examples: [\"YYYY-MM-DD\", \"MM/DD/YYYY\", \"DD-MMM-YYYY HH:mm:ss\"]\n extractDateTimezone:\n type: string\n description: |\n Specifies the timezone of the input date string using Olson/IANA timezone identifiers.\n\n Used on string-typed mappings whose `extract` yields a date; tells the system\n how to interpret timestamp values from the input system.\n examples: [\"UTC\", \"America/New_York\", \"Europe/London\"]\n generateDateFormat:\n type: string\n description: |\n Specifies the output format pattern when generating a date string or converting\n from a Date type to String type.\n\n Uses Moment.js-compatible formatting tokens to define the structure of the resulting\n date string.\n examples: [\"YYYY-MM-DD\", \"MM/DD/YYYY\", \"DD-MMM-YYYY HH:mm:ss\"]\n generateDateTimezone:\n type: string\n description: |\n Specifies the timezone to apply when generating or converting timestamp values\n using Olson/IANA timezone identifiers.\n\n Controls timezone conversion when producing date output.\n examples: [\"UTC\", \"America/New_York\", \"Europe/London\"]\n default:\n type: [string, 'null']\n description: |\n Specifies a fallback value to use when extract returns empty/null or when conditional\n logic fails and no other mapping supplies a value.\n\n Explicit JSON `null` is itself a valid fallback: the destination field is written as\n JSON null when the extract yields nothing (the Mapper UI's \"Use null as default value\"\n action). Omitting the key entirely means no fallback — the field is left out of the\n output when the extract is missing or an empty string (the UI's \"Do nothing\" action),\n while a null source is still written as null unless `conditional.when` is\n `extract_not_empty`. Failing the record instead is `conditional.when:\n fail_record_when_empty`, which excludes a default.\n x-celigo-ai-guidance:\n - |-\n `\"default\": null` and `\"default\": \"\"` are distinct saved values, not\n absent keys: null renders the destination field as JSON null and the\n empty string renders it as `\"\"` when the extract yields nothing.\n Preserve them verbatim when editing or migrating a mapping — dropping\n the key changes runtime behavior from \"use null/empty string as\n default value\" to \"do nothing\" (a missing or `\"\"` source is left out\n of the output, while a null source is written as null unless\n `conditional.when` is `extract_not_empty`).\n - |-\n The four \"Action to take if source field has no value\" choices map to:\n do nothing → no `default` key (add `conditional.when:\n extract_not_empty` to leave a null source out too; without it a null\n source is written as null); use null → `\"default\": null`; use empty string or\n a custom value → `\"default\": \"\u003cvalue>\"`; cause the record to fail →\n `conditional.when: fail_record_when_empty` with no `default` key.\n examples: [\"N/A\", \"0\", \"false\"]\n lookupName:\n type: string\n description: |\n **Purpose**\n References a lookup table for transforming values during the mapping process.\n\n **Usage**\n\n The lookupName refers to a named lookup defined in the lookups array of the same resource.\n On an HTTP import that is `http.lookups`: a `lookups` array sent only at the top level\n is dropped on save. On an import, a name that matches no lookup fails every record\n with \"Unable to find lookup in mapping configs - \u003cname>\", and a key the lookup\n doesn't hold follows the lookup's `allowFailures`: false fails the record; true, or\n the field left out, lets the record succeed with the lookup's `default`, or without\n the mapped field when there is none.\n\n ```json\n {\n \"generate\": \"countryName\",\n \"dataType\": \"string\",\n \"extract\": \"$.countryCode\",\n \"lookupName\": \"countryCodeToName\"\n }\n ```\n\n During processing, the system:\n 1. Extracts the value from the input record (e.g., \"US\")\n 2. Finds the lookup table with the specified name\n 3. Uses the extracted value as a key in the lookup\n 4. Returns the corresponding value (e.g., \"United States\")\n\n **Benefits**\n\n - **Standardization**: Ensures consistent value translation across mappings\n - **Centralization**: Define translations once and reference them in multiple places\n - **Maintainability**: Update all mappings by changing the lookup definition\n - **Readability**: Makes mappings more descriptive and self-documenting\n\n The specific lookup capabilities depend on the context where mappings are used.\n examples: [\"countryCodeToName\", \"statusMapping\", \"departmentCodes\"]\n hardCodedValue:\n type: string\n description: |\n Constant written to the destination instead of an extracted value. On a primitive\n array (`stringarray`, `numberarray`, `booleanarray`) the string is split on commas,\n verbatim and untrimmed, into elements of the array's type (`\"A,B\"` gives `[\"A\", \"B\"]`).\n On an `object` it must be a JSON object string and on an `objectarray` a JSON array\n string (an object string becomes one element); the save parses it and refuses non-JSON\n or the wrong shape with 422, and an array row with a constant needs no `buildArrayHelper`.\n examples: [\"USD\", \"A,B\", \"[{\\\"code\\\": \\\"A\\\"}, {\\\"code\\\": \\\"B\\\"}]\"]\n description:\n type: string\n description: |\n Optional free-text annotation that appears in the Mapper sidebar to provide context about\n the mapping's purpose for collaboration and documentation.\n\n Has no functional impact on the mapping behavior.\n examples: [\"Maps customer name\", \"Converts price to target currency\", \"Formats date for target system\"]\n sourceDataType:\n type: string\n description: |\n Declares the JSON type of the value produced by `extract`, before any\n conversion to `dataType`. Same enum as `dataType`. Set on leaf mappings\n only — parent mappings (with child `mappings` or `buildArrayHelper`)\n have no extracted value of their own; the children carry their own\n `sourceDataType`.\n\n For date fields use `string` (JSON represents dates as strings); the\n parsing/formatting lives in `extractDateFormat` / `generateDateFormat` /\n `extractDateTimezone` / `generateDateTimezone`.\n enum:\n - string\n - number\n - boolean\n - object\n - stringarray\n - numberarray\n - booleanarray\n - objectarray\n - arrayarray\n x-enumDescriptions:\n string: Source value is a text string.\n number: Source value is numeric.\n boolean: Source value is true or false.\n object: Source value is a JSON object.\n stringarray: Source value is an array of strings.\n numberarray: Source value is an array of numbers.\n booleanarray: Source value is an array of booleans.\n objectarray: Source value is an array of objects.\n arrayarray: Source value is an array of arrays.\n examples: [\"string\", \"number\", \"boolean\", \"object\"]\n mappings:\n type: array\n description: |\n **Purpose**\n Enables recursive definition of nested object structures through child mapping objects.\n\n **Guidance**\n This is the KEY FIELD that implements the recursive pattern at the core of this schema:\n\n **When to Use**\n - REQUIRED when dataType = \"object\" (unless you are copying an entire object from the input record)\n - REQUIRED in buildArrayHelper.mappings when defining complex object array elements\n - NEVER used with simple types (string, number, boolean)\n\n **Behavior**\n - Each mapping in this array becomes a property of the parent object\n - The full Mappings schema is repeated recursively at each level\n - Can be nested to any depth for complex hierarchical structures\n\n **Context Handling**\n - Each level of nesting changes the mapping CONTEXT for 'generate'\n - The extraction CONTEXT always remains the original input record\n - This means child mappings can pull data from anywhere in the input record\n\n **Common Patterns**\n\n **Nested Objects**\n ```json\n {\n \"generate\": \"customer\",\n \"dataType\": \"object\",\n \"mappings\": [\n {\n \"generate\": \"contact\",\n \"dataType\": \"object\",\n \"mappings\": [\n {\"generate\": \"email\", \"dataType\": \"string\", \"extract\": \"$.customerEmail\"}\n ]\n }\n ]\n }\n ```\n\n **Multiple Fields in Object**\n ```json\n {\n \"generate\": \"address\",\n \"dataType\": \"object\",\n \"mappings\": [\n {\"generate\": \"street\", \"dataType\": \"string\", \"extract\": \"$.address.line1\"},\n {\"generate\": \"city\", \"dataType\": \"string\", \"extract\": \"$.address.city\"},\n {\"generate\": \"country\", \"dataType\": \"string\", \"extract\": \"$.address.country\"}\n ]\n }\n ```\n\n IMPORTANT: When analyzing or generating mappings, ALWAYS check if parent.dataType = \"object\"\n or if you're inside buildArrayHelper.mappings for objectarray elements. These are the only\n valid contexts for the mappings array.\n items:\n $ref: '#/Mappings/items'\n examples: [[{\"generate\": \"firstName\", \"extract\": \"$.name.first\", \"dataType\": \"string\", \"status\": \"Active\"}, {\"generate\": \"lastName\", \"extract\": \"$.name.last\", \"dataType\": \"string\", \"status\": \"Active\"}]]\n buildArrayHelper:\n type: array\n x-celigo-ai-guidance:\n - |-\n Entry-level `extract` accepts an iterating array path\n (`$.items[*]`), `\"$\"` (the record root), or omission — a\n constructed entry (child `mappings` with the extract omitted or\n `\"$\"`; the two spellings are equivalent) contributes exactly one\n element, and entries append in order. On a grouped (array)\n record the record itself is the array to iterate: `\"$[*]\"`\n yields one `objectarray` element per row (children address the\n row as `$.field`), and `\"$[*].field\"` yields one primitive-array\n value per row. Indexed or comma-joined\n entry extracts (`\"$[0]\"`, `\"$[0],$[0]\"`) are not valid: the\n processor renders the entire destination array as null with no\n validation error. To emit a fixed number of static elements, add\n that many sibling constructed entries — never index the extract.\n - |-\n An entry's `extract` can be a Handlebars template. On an\n `objectarray` it must render a JSON object (one element) or a JSON\n array (one element per item). On a primitive array each entry\n renders one element, so a serialized array becomes one string\n element. A bare literal entry extract is refused with 422 (\"extract\n only supports json path or handlebars expression\"); set a constant\n with the row's `hardCodedValue` instead.\n description: |\n **Purpose**\n Configures how to construct arrays in the output record, handling various array types and inputs.\n\n **Guidance**\n This is the mechanism for building array data types:\n\n **When to Use**\n - REQUIRED when dataType ends with \"array\" (stringarray, objectarray, etc.), unless the\n row sets `hardCodedValue`: an array row's own `extract` is never read (see `dataType`)\n - Each entry in this array contributes elements to the output array\n - Multiple entries allow combining data from different input arrays\n\n **Array Type Handling**\n\n **For Simple Arrays (stringarray, numberarray, booleanarray)**\n - Only the 'extract' field is used to pull values\n - JSON path with wildcards (e.g., $.items[*].name) returns multiple values;\n on a grouped (array) record the record itself is the array, so the\n path roots at it ($[*].name — one value per row)\n - Each result is converted to the appropriate primitive type\n ```json\n {\n \"generate\": \"productNames\",\n \"dataType\": \"stringarray\",\n \"buildArrayHelper\": [\n {\"extract\": \"$.products[*].name\"}\n ]\n }\n ```\n\n **For Object Arrays (objectarray) - three patterns**\n\n 1. Extract Only (existing objects):\n ```json\n {\n \"generate\": \"contacts\",\n \"dataType\": \"objectarray\",\n \"buildArrayHelper\": [\n {\"extract\": \"$.account.primaryContacts[*]\"}, // Pull primary contact objects\n {\"extract\": \"$.account.secondaryContacts[*]\"}, // Pull secondary contact objects\n {\"extract\": \"$.vendor.contactPersons[*]\"}, // Pull vendor contact objects\n {\"extract\": \"$.subsidiaries[*].mainContact\"} // Pull main contact from each subsidiary\n ]\n }\n ```\n\n 2. Mappings Only (constructed object):\n ```json\n {\n \"generate\": \"contactInfo\",\n \"dataType\": \"objectarray\",\n \"buildArrayHelper\": [\n {\n \"mappings\": [ // Creates one object in the array\n {\"generate\": \"type\", \"dataType\": \"string\", \"extract\": \"primary\"},\n {\"generate\": \"email\", \"dataType\": \"string\", \"extract\": \"$.primaryEmail\"}\n ]\n },\n {\n \"mappings\": [ // Creates another object in the array\n {\"generate\": \"type\", \"dataType\": \"string\", \"extract\": \"secondary\"},\n {\"generate\": \"email\", \"dataType\": \"string\", \"extract\": \"$.secondaryEmail\"}\n ]\n }\n ]\n }\n ```\n Each constructed entry contributes exactly one element, in entry\n order; writing the entry with `\"extract\": \"$\"` is equivalent to\n omitting the extract (both anchor the element at the record root).\n A fixed number of static elements is built from that many sibling\n constructed entries.\n\n 3. Extract AND Mappings (transform input arrays):\n ```json\n {\n \"generate\": \"lineItems\",\n \"dataType\": \"objectarray\",\n \"buildArrayHelper\": [\n {\n \"extract\": \"$.order.items[*]\", // For each item in the array\n \"mappings\": [ // Transform to this structure using the composite object\n {\"generate\": \"sku\", \"dataType\": \"string\", \"extract\": \"$.order.items.productId\"}, // Notice: items is singular\n {\"generate\": \"quantity\", \"dataType\": \"number\", \"extract\": \"$.order.items.qty\"}, // Notice: items is singular\n {\"generate\": \"orderNumber\", \"dataType\": \"string\", \"extract\": \"$.order.id\"}, // Access parent data\n {\"generate\": \"customerName\", \"dataType\": \"string\", \"extract\": \"$.customerName\"} // Access root data\n ]\n }\n ]\n }\n ```\n\n **Arrays of Arrays**\n - The mapper has no working type for them: `arrayarray` saves, but every record\n fails with `Destination datatype is a required field.` Build an array of arrays\n in a script hook instead.\n\n **Important details**\n\n - When both extract and mappings are provided, the system creates special composite objects\n that maintain hierarchical context during processing\n - This enables accessing both the current array element AND its parent context\n - An ITERATING entry's extract MUST be a JSON path that iterates an array:\n '$.items[*]' when the array is a field of an object record, or '$[*]'\n (objectarray) / '$[*].field' (primitive arrays) when the record itself is a\n grouped array of rows. A CONSTRUCTED entry (child mappings building one\n element) instead omits extract or sets it to exactly \"$\" — never rewrite \"$\"\n into an indexed or comma-joined form ('$[0]', '$[0],$[0]'): those render the\n entire destination array as null without raising a validation error\n - Each array helper entry acts independently, potentially adding multiple elements\n\n The buildArrayHelper is the most complex part of the mappings system - always analyze the\n dataType first to understand which pattern is appropriate.\n items:\n type: object\n properties:\n extract:\n type: string\n description: |\n JSON path expression that identifies the input array or values to extract.\n\n For objectarray with mappings, this defines which input objects to iterate through.\n The JSON path must return either a single object or an array of objects.\n\n For a CONSTRUCTED entry (child mappings building exactly one element from\n record-root fields and constants), omit this field or set it to exactly \"$\" —\n the two spellings are equivalent. Indexed or comma-joined forms ('$[0]',\n '$[0],$[0]') are not valid at the entry level: the processor renders the\n entire destination array as null without raising a validation error.\n\n The system creates special composite objects during processing to maintain\n hierarchical relationships, allowing easy access to both the current array item\n and its parent contexts.\n\n An iterating entry keeps the `[*]`: `$.line_items` without it yields one empty\n element per item, with no error. Child extracts drop the `[*]` but keep the\n array's path (`$.line_items.sku`); `$.line_items[*].sku` maps nothing, and an\n element-relative `$.sku` reads the record root instead of the element.\n examples: [\"$.items[*]\", \"$.order.lineItems[*]\", \"$.parents[*].children[*]\", \"$\"]\n name:\n type: string\n maxLength: 200\n description: |\n Display label for this source object in the mapper, so the sources of\n an object array can be told apart instead of all showing as ` building-tools — Skillopedia .\n Has no effect at runtime; stored only when non-empty.\n x-celigo-ai-guidance:\n - |-\n Carry an existing `name` through every edit — it is the user's label\n for the tab, and an entry sent without it loses the label. Duplicate\n names across entries are allowed. Set one only when the user names\n the source object; never invent labels.\n examples: [\"Billing address\", \"Shipping address\"]\n sourceDataType:\n type: string\n description: |\n Declares the JSON type of the input array being iterated, to ensure\n proper type handling during array construction. Same enum as `dataType`.\n enum:\n - string\n - number\n - boolean\n - object\n - stringarray\n - numberarray\n - booleanarray\n - objectarray\n - arrayarray\n x-enumDescriptions:\n string: Input array contains string elements.\n number: Input array contains numeric elements.\n boolean: Input array contains boolean elements.\n object: Input array contains object elements.\n stringarray: Input array contains arrays of strings.\n numberarray: Input array contains arrays of numbers.\n booleanarray: Input array contains arrays of booleans.\n objectarray: Input array contains arrays of objects.\n arrayarray: Input array contains arrays of arrays.\n examples: [\"string\", \"number\", \"boolean\", \"object\"]\n default:\n type: [string, 'null']\n description: |\n Specifies a fallback value when the extracted array element is empty or\n not found in the input data.\n\n Explicit JSON `null` is itself a valid fallback (the element is written as\n JSON null); omitting the key means no fallback is applied.\n examples: [\"N/A\", \"0\", \"false\"]\n conditional:\n type: object\n description: |\n Defines conditional rules for including each array element in the result.\n x-celigo-ai-guidance:\n - |-\n Judged against this entry's own `extract` (the array source). The\n design scopes the UI's tab-level \"Action to take if source field has\n no value\" to copy-as-is tabs (no child `mappings`); the API accepts\n `fail_record_when_empty` on a mapped entry as well, and the mapper\n processor honors it there — the record fails when that array is\n missing or empty (`errors[]` entry `code: extract_empty`, `fatal:\n true`, message `\u003cgenerate>: Source field has no value`; probed\n 2026-10-09). Say so when writing it on a mapped entry: the user may\n not find the setting in the mapper's tab. To fail on an empty field\n inside each element, set the action on that child mapping instead.\n properties:\n when:\n type: string\n description: |\n Condition judged on the entry's extract — skip the elements the\n extract does not return, or fail the whole record when the source\n array is missing or empty.\n enum:\n - extract_not_empty\n - fail_record_when_empty\n x-enumDescriptions:\n extract_not_empty: Include an array element only when the extract returns a non-empty value for it\n fail_record_when_empty: Fail the record when the entry's source array is missing or empty\n examples: [\"extract_not_empty\"]\n mappings:\n type: array\n description: |\n Contains recursive mapping definitions for complex array element transformations.\n\n **Composite object mechanism**\n\n When both 'extract' and 'mappings' are used together, the system implements a sophisticated\n \"composite object\" approach that is crucial for AI agents to understand:\n\n 1. The system starts with the complete input record\n\n 2. For each array element matched by the extract path, it creates a modified version of\n the input record where:\n - Array paths in the extract JSON path are REPLACED with single objects\n - Each array ([]) in the path is converted to a single object ({})\n - This preserves the hierarchical relationship between nested arrays\n\n **Example**\n\n Given an input record:\n ```json\n {\n \"customer\": {\n \"name\": \"John Doe\",\n \"orders\": [\n {\n \"id\": \"O-001\",\n \"items\": [\n {\"sku\": \"ABC\", \"qty\": 2},\n {\"sku\": \"XYZ\", \"qty\": 1}\n ]\n },\n {\n \"id\": \"O-002\",\n \"items\": [\n {\"sku\": \"DEF\", \"qty\": 3}\n ]\n }\n ]\n }\n }\n ```\n\n With extract path: `$.customer.orders[*].items[*]`\n\n For each item, the system creates a composite object like:\n ```json\n {\n \"customer\": {\n \"name\": \"John Doe\",\n \"orders\": { // Note: Array replaced with single object\n \"id\": \"O-001\",\n \"items\": { // Note: Array replaced with single object\n \"sku\": \"ABC\",\n \"qty\": 2\n }\n }\n }\n }\n ```\n\n Then in your mappings, you can access:\n - The current item: `$.customer.orders.items.sku`\n - The parent order: `$.customer.orders.id`\n - Top-level data: `$.customer.name`\n\n This approach allows for precise mapping from deeply nested structures while maintaining\n access to all contextual parent data, without requiring complex array index management.\n\n **Implementation guidance**\n\n When implementing the composite object mechanism:\n\n 1. Analyze the extract path to identify all array patterns (`[*]` or `[number]`)\n 2. For each array in the path, understand that it will be replaced with a single object\n 3. In the mappings, use paths that reference these arrays as if they were objects\n 4. Remember that every mapping still has access to the full input record context\n 5. This mechanism is especially powerful when mapping hierarchical data like:\n - Order → Line Items → Taxes/Discounts\n - Customer → Addresses → Address Lines\n - Invoice → Line Items → Serial Numbers\n\n The extract path effectively tells the system \"iterate through these arrays\",\n while the composite object mechanism ensures you can still access both the\n current array item AND its parent context during mapping.\n items:\n $ref: '#/Mappings/items'\n examples: [[{\"generate\": \"itemName\", \"extract\": \"$.name\", \"dataType\": \"string\", \"status\": \"Active\"}, {\"generate\": \"itemPrice\", \"extract\": \"$.price\", \"dataType\": \"number\", \"status\": \"Active\"}]]\n # The save rejects the fail-record action paired with a default on an\n # entry ([unsupported_mapping]); the entry schema has no lookupName or\n # hardCodedValue, so this is the only pair at this level.\n if:\n properties:\n conditional:\n properties:\n when:\n const: fail_record_when_empty\n required: [when]\n required: [conditional]\n then:\n not:\n required: [default]\n status:\n type: string\n description: |\n **Purpose**\n Controls whether the mapping is applied. Send it on every mapping entry.\n\n **Guidance**\n Only `Active` rows run. `Draft` and `Inactive` rows keep their\n configuration, are saved with minimal validation, and are skipped at\n runtime and in preview; a non-Active parent skips its whole subtree\n whatever its children say. Imports (on save and in the mapper processor)\n and API response mappings reject a row missing this field (validation\n error: \"Mapping object must have status field present.\"); a Tool's\n output mapping saves and runs one, which is why `required` doesn't\n list it.\n x-celigo-ai-guidance:\n - |-\n Emit `\"Active\"` for mappings that should run; `\"Draft\"` is also\n accepted and saves the mapping while bypassing the Active-only\n field validations. The value is case-sensitive. The API rejects:\n - Missing field: `[status_field_absent] Mapping object must have status field present.`\n - Wrong case (e.g. `\"active\"`): `[invalid_status_enums] active is not a valid enum for status field.`\n - |-\n `\"Inactive\"` is the user's \"Set inactive\" on a row: use it only when\n asked to disable or park a row, never to mean incomplete (that is\n `\"Draft\"`). Setting a parent `Inactive` means no `Active` row may\n remain under it — the save rejects an `Active` row under an\n `Inactive` parent (`[inactive_parent_active_child]` on `mappings`,\n no message text) and never cascades or promotes on its own; `Draft`\n descendants are kept as they are, and a `Draft` parent does not gate\n its children. The UI's Set inactive on a parent marks the parent and\n every child `Inactive`. Re-enabling a child\n means re-enabling its ancestors in the same payload. Edits that keep\n existing rows must carry their stored status forward — rewriting a\n `Draft` or `Inactive` row to `Active` turns a row the user parked\n back on.\n enum:\n - Active\n - Draft\n - Inactive\n x-enumDescriptions:\n Active: The mapping is enabled and applied during processing\n Draft: In-progress mapping; saved without the Active-only field validations and skipped at runtime\n Inactive: Disabled by the user; configuration kept, skipped at runtime and in preview\n examples: [\"Active\"]\n conditional:\n type: object\n description: |\n **Purpose**\n Defines conditional processing rules for the entire mapping.\n\n **Guidance**\n These conditions determine whether the mapping is applied based on record\n state or field content:\n\n **When to Use**\n - When a mapping should only be applied in specific circumstances\n - To implement conditional logic without using complex handlebars expressions\n - For creating mappings that only run during create or update operations\n\n **Available Conditions**\n\n - `record_created`: Apply only when creating a new record\n Useful for setting initial values that should not be overwritten during updates\n\n - `record_updated`: Apply only when updating an existing record\n Useful for transformation logic that should only run during updates\n\n - `extract_not_empty`: Apply only when the extract field returns a value\n Useful for conditional mapping based on input data availability\n\n - `fail_record_when_empty`: Fail the record when the extract field returns\n no value (the UI's \"Cause record to fail\" action when the source field\n has no value)\n\n **Example**\n ```json\n {\n \"generate\": \"statusMessage\",\n \"dataType\": \"string\",\n \"status\": \"Active\",\n \"extract\": \"$.status.message\",\n \"conditional\": {\n \"when\": \"extract_not_empty\" // Only map when status.message exists\n }\n }\n ```\n x-celigo-ai-guidance:\n - |-\n `when` holds one value, and two different settings share it: the\n composite-import operation gate (`record_created` / `record_updated`,\n the UI's \"Only perform mapping when\") and the empty-source action\n (`extract_not_empty` / `fail_record_when_empty`, the UI's \"Action to\n take if source field has no value\"). A row cannot carry both — writing\n one replaces the other and the save succeeds, so a composite import\n row that needs both has to choose. Never author `fail_record_when_empty`\n on a row that already carries `record_created` or `record_updated`.\n properties:\n when:\n type: string\n description: |\n Condition that gates this mapping — either an operation gate on a\n composite (create-or-update) import or the action to take when the\n extract returns no value. Empty means null, undefined or an empty\n string; `0`, `false` and whitespace are values.\n x-celigo-ai-guidance:\n - |-\n `fail_record_when_empty` is the fourth \"Action to take if source field\n has no value\" and is mutually exclusive with the other three: send it\n with no `default` key. A `default`, a `lookupName` or a `hardCodedValue`\n on the same row — or a `default` on a `buildArrayHelper` entry — is\n rejected at save with `[unsupported_mapping] \"\u003ckey>\" and conditional.when\n \"fail_record_when_empty\" cannot be present in mapping simultaneously.`\n (the lookup variant adds `Use lookup \"allowFailures\" instead.`);\n lookups fail through `allowFailures: false`, and a hard-coded row\n always has a value. At runtime an empty extract fails the record with\n an `errors[]` entry `{code: \"extract_empty\", fatal: true, source:\n \"mapping\", message: \"\u003cgenerate>: Source field has no value\"}`\n (\"Input field has no value\" in a Transformation 2.0); `0` and `false`\n are values and map through.\n enum:\n - record_created\n - record_updated\n - extract_not_empty\n - fail_record_when_empty\n x-enumDescriptions:\n record_created: Apply this mapping only when creating a new record\n record_updated: Apply this mapping only when updating an existing record\n extract_not_empty: Apply this mapping only when the extract field returns a non-empty value\n fail_record_when_empty: Fail the record when the extract field returns no value\n examples: [\"record_created\", \"record_updated\", \"extract_not_empty\", \"fail_record_when_empty\"]\n required:\n - dataType\n if:\n properties:\n conditional:\n properties:\n when:\n const: fail_record_when_empty\n required: [when]\n required: [conditional]\n then:\n not:\n anyOf:\n - required: [default]\n - required: [lookupName]\n - required: [hardCodedValue]\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":52991,"content_sha256":"52bc801b81ac5f934dda6ea0e194361e1aad6592840f911cf948db2fbb4ca41d"},{"filename":"references/common-schemas/resource-response.yml","content":"ResourceResponse:\n type: object\n description: Response\n x-celigo-ai-guidance:\n - Core response fields shared by all Celigo resources\n properties:\n _id:\n type: string\n format: objectId\n readOnly: true\n description: Unique identifier for the resource. Format is a 24-character hexadecimal string.\n x-celigo-ai-guidance:\n - |-\n The _id is used in:\n - API endpoints that operate on a specific resource (e.g., GET, PUT, DELETE)\n - References from other resources (e.g., flows that use this resource)\n - Job history and error tracking\n examples: [\"5f8d43a1b9e5a80011a35f2c\"]\n createdAt:\n type: string\n format: date-time\n readOnly: true\n description: Timestamp when the resource was created. Set automatically and cannot be modified.\n x-celigo-ai-guidance:\n - Recorded in ISO 8601 with a UTC timezone (Z suffix).\n examples: [\"2023-04-01T09:15:32Z\"]\n lastModified:\n type: string\n format: date-time\n readOnly: true\n description: Timestamp when the resource was last updated. Changes whenever any property is modified.\n x-celigo-ai-guidance:\n - Recorded in ISO 8601 with a UTC timezone (Z suffix); always equal to or later than `createdAt`.\n - |-\n Records that something was written, not what — a settings edit, a flag flip, or a\n platform workflow all move it. What changed and when is in the audit log: the\n per-resource route `GET /v1/{resourceType}/{_id}/audit` for the types its\n `resourceType` enum lists, or the account-wide\n `GET /v1/audit?resourceType=\u003csingular>&_resourceId={_id}` for the further types that\n endpoint's enum accepts (users, environments, notifications among them). A type absent\n from both enums — jobs, connectors, templates — has only this timestamp.\n examples: [\"2023-04-15T14:30:15Z\"]\n deletedAt:\n type: [string, 'null']\n format: date-time\n readOnly: true\n description: Timestamp when the resource was soft-deleted. When null or absent, the resource is active.\n x-celigo-ai-guidance:\n - Non-null means the resource is soft-deleted (in the recycle bin) — recoverable within the retention period before permanent removal.\n examples: [\"2023-05-20T11:45:32Z\"]\n required:\n - _id\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":2380,"content_sha256":"545364158eb63d4407ea79b5957cc357b55411ee852257bb240a60d88741bb70"},{"filename":"references/common-schemas/transform.yml","content":"Transform:\n type: object\n description: |\n Configuration for transforming data during processing operations. This object enables\n reshaping of records.\n\n **Transformation capabilities**\n\n Celigo's transformation engine offers powerful features for data manipulation:\n - Precise field mapping with JSONPath expressions\n - Support for any level of nested arrays\n - Formula-based field value generation\n - Dynamic references to flow and integration settings\n\n **Implementation approaches**\n\n There are two distinct transformation mechanisms available:\n\n **Rule-Based Transformation (`type: \"expression\"`)**\n - **Best For**: Most transformation scenarios from simple to complex\n - **Capabilities**: Field mapping, formula calculations, lookups, nested data handling\n - **Advantages**: Visual configuration, no coding required, intuitive interface\n - **Configuration**: Define rules in the `expression` object\n - **Use When**: You have clear mapping requirements or need to reshape data structure\n\n **Script-Based Transformation (`type: \"script\"`)**\n - **Best For**: Extremely complex logic or proprietary algorithms\n - **Capabilities**: Full programmatic control, custom processing, complex business rules\n - **Advantages**: Maximum flexibility, can implement any transformation logic\n - **Configuration**: Reference a script in the `script` object\n - **Use When**: Visual transformation tools aren't sufficient for your use case\n properties:\n type:\n type: string\n description: |\n Determines which transformation mechanism to use. This choice affects which properties\n must be configured and how transformation logic is implemented.\n\n **Available types**\n\n **Rule-Based Transformation (`\"expression\"`)**\n - **Required Config**: The `expression` object with mapping definitions\n - **Behavior**: Applies declarative rules to reshape data\n - **Best For**: Most transformation scenarios from simple to complex\n - **Advantages**: Visual configuration, no coding required\n\n **Script-Based Transformation (`\"script\"`)**\n - **Required Config**: The `script` object with _scriptId and function\n - **Behavior**: Executes custom JavaScript to transform data\n - **Best For**: Extremely complex logic or proprietary algorithms\n - **Advantages**: Maximum flexibility, can implement any logic\n\n **Implementation guidance**\n\n 1. For standard data transformations, use `\"expression\"`\n 2. For complex logic or specialized processing, use `\"script\"`\n 3. When selecting a type, you must configure the corresponding object:\n - `type: \"expression\"` requires the `expression` object\n - `type: \"script\"` requires the `script` object\n enum: [\"expression\", \"script\"]\n x-enumDescriptions:\n expression: Rule-based transformation using declarative mapping definitions.\n script: Script-based transformation using custom JavaScript logic.\n examples: [\"expression\", \"script\"]\n expression:\n type: object\n description: |\n Configuration for declarative rule-based transformations. This object enables reshaping data\n without requiring custom code.\n\n **Usage context**\n\n This object is REQUIRED when `transform.type` is set to \"expression\" and should not be\n configured otherwise. It provides a standardized way to define transformation rules that\n can map, modify, and generate data elements.\n\n **Implementation guidance**\n\n The expression system uses a rule-based approach where:\n - Field mappings define how input data is transformed to target fields\n - Formulas can be used to calculate or generate new values\n - Lookups can enrich data by fetching related information\n - Mode determines how records are processed (create new or modify existing)\n properties:\n version:\n type: string\n description: |\n Version of the expression format. Determines which rules\n property contains the transformation logic.\n enum: [\"1\", \"2\"]\n x-enumDescriptions:\n \"1\": \"Legacy format. Rules are in the `rules` array (array of arrays of extract/generate objects).\"\n \"2\": \"Current format. Rules are in the `rulesTwoDotZero` object with mode, mappings, and lookups.\"\n examples: [\"1\", \"2\"]\n rules:\n type: array\n description: |\n Transformation rules for version 1 expressions. An array of\n rule groups; each group is an array of field-mapping objects.\n Most transforms have a single group. Present when `version`\n is `\"1\"`. The output record contains ONLY the generated\n fields — every unmapped field is dropped (v1 has no\n equivalent of Transform 2.0's `modify` mode), and the\n record's trace key does not survive the rebuild.\n x-celigo-ai-guidance:\n - |-\n v1 rules REPLACE the record: a one-field rule group drops\n every other field, silently breaking downstream filters,\n router branch rules, and templates that referenced the\n dropped fields. Either map every field downstream logic\n needs forward, or author Transform 2.0 with\n ``mode: \"modify\"`` instead.\n - |-\n An import's ``responseTransform`` is the exception: it accepts\n version ``\"1\"`` only (``\"2\"`` is refused with 422, verified live\n 2026-10-21), so there, map every response field that response\n mapping needs.\n items:\n type: array\n items:\n type: object\n properties:\n extract:\n type: string\n description: |\n Source field path to read from. Supports multiple\n syntaxes: bare field names (`id`), dot notation\n (`fulfillment.shipment_id`), slash-prefixed paths\n for XML (`/FeedProcessingStatus`), wildcards (`*.id`,\n `*.[Internal ID]`), and array indexing (`SDF[0]`).\n generate:\n type: string\n description: |\n Target field name to write to. Typically a bare name\n (`id`) or dot path (`SDF.Filter.ID`).\n key:\n type: string\n description: |\n Auto-generated identifier for this rule, used by the\n UI to track individual rules for editing and reordering.\n required:\n - extract\n - generate\n rulesTwoDotZero:\n type: object\n description: |\n Configuration for version 2 transformation rules. This object contains the core logic\n for how data is mapped, enriched, and transformed.\n\n **Capabilities**\n\n Transformation 2.0 provides:\n - Precise field mapping with JSONPath expressions\n - Support for deeply nested data structures\n - Formula-based field generation\n - Dynamic lookups for data enrichment\n - Multiple operating modes to fit different scenarios\n properties:\n mode:\n type: string\n description: |\n Transformation mode that determines how records are handled during processing.\n\n **Available modes**\n\n **Create Mode (`\"create\"`)**\n - **Behavior**: Builds entirely new output records from inputs\n - **Use When**: Output structure differs significantly from input\n - **Advantage**: Clean slate approach, no field inheritance\n\n **Modify Mode (`\"modify\"`)**\n - **Behavior**: Makes targeted edits to existing records\n - **Use When**: Output structure should remain similar to input\n - **Advantage**: Preserves unmapped fields from the original record\n enum: [\"create\", \"modify\"]\n x-enumDescriptions:\n create: Builds entirely new output records from inputs, ignoring unmapped fields.\n modify: Makes targeted edits to existing records, preserving unmapped fields.\n x-celigo-ai-guidance:\n - |-\n On a grouped record (the export groups rows, so the record is an\n array) only `create` can author output — build it from the rows with\n `$[0].field`, `$[*].field`, or a `buildArrayHelper` over `$[*]`.\n `modify` has no object to overlay onto an array: the record passes\n through untouched and the processor reports success. Either mode\n reshapes the inside of one record; neither changes how many records\n the page carries (that is the export's `preSavePage` hook) or whether\n the export groups at all (that is its grouping setting).\n examples: [\"create\", \"modify\"]\n mappings:\n $ref: './mappings.yml#/Mappings'\n lookups:\n allOf:\n - description: |\n Shared lookup tables used across all mappings defined in the transformation rules.\n\n **Purpose**\n\n Lookups provide centralized value translation that can be referenced from any mapping\n in your transformation configuration. They enable consistent translation of codes, IDs,\n and values between systems without duplicating translation logic.\n\n **Usage in transformations**\n\n Lookups are particularly valuable in transformations for:\n\n - **Data Normalization**: Standardizing values from diverse source systems\n - **Code Translation**: Converting between different coding systems (e.g., status codes)\n - **Field Enrichment**: Adding descriptive values based on ID or code lookups\n - **Cross-Reference Resolution**: Mapping identifiers between integrated systems\n\n **Implementation**\n\n Lookups are defined once in this array and referenced by name in mappings:\n\n ```json\n \"lookups\": [\n {\n \"name\": \"statusMapping\",\n \"map\": {\n \"A\": \"Active\",\n \"I\": \"Inactive\",\n \"P\": \"Pending\"\n },\n \"default\": \"Unknown Status\"\n }\n ]\n ```\n\n Then referenced in mappings using the lookupName property:\n\n ```json\n {\n \"generate\": \"status\",\n \"dataType\": \"string\",\n \"extract\": \"$.statusCode\",\n \"lookupName\": \"statusMapping\"\n }\n ```\n\n The system automatically applies the lookup during transformation processing.\n\n For complete details on lookup properties and behavior, see the Lookups schema.\n - $ref: './lookups.yml#/Lookups'\n inputContext:\n type: string\n enum:\n - record\n - envelope\n x-enumDescriptions:\n record: The transform processor evaluates `mappings[].extract` JSONPath against the flat record itself (`$.field`, no `record.` segment) — the default when absent.\n envelope: The transform processor evaluates JSONPath against the wrapped runtime envelope `{record, job, testMode, settings, headers, queryParams}` (file-provider steps also carry `fileMeta`) — record fields need the `record.` segment (`$.record.field`), while `headers` and `queryParams` sit beside `record` (`$.headers['X-Id']`, `$.queryParams.src`).\n description: |\n Controls the JSON shape the transformTwoDotZero processor\n evaluates `mappings[].extract` JSONPath values against at\n flow runtime. Applies only to Transform 2.0 (v2,\n `rulesTwoDotZero`); v1 transforms (the `rules` array on\n `transform.expression.rules`) and script-mode transforms\n ignore this field.\n x-celigo-ai-guidance:\n - |-\n ## Extract anchoring by mode\n\n - `record` (default — absent / empty / undefined): the\n processor sees the FLAT record as its evaluation root.\n `mappings[].extract` JSONPath MUST anchor directly at\n ` building-tools — Skillopedia with NO `record.` segment — e.g. `\"$.orderId\"`,\n `\"$.customer.email\"`; PLACEHOLDER extracts use\n `\"$.PLACEHOLDER_\u003cfield>\"`.\n This is the dominant Celigo idiom and the right choice for new transforms.\n - `envelope`: the processor sees a wrapped object\n `{record: {...}, job, testMode, settings, headers,\n queryParams}` (plus `fileMeta` on file-provider steps).\n `mappings[].extract` JSONPath MUST include the\n `record.` segment for record fields — e.g.\n `\"$.record.orderId\"` — and reads `headers` and\n `queryParams` beside it (`$.headers['X-Id']`);\n PLACEHOLDER extracts use `\"$.record.PLACEHOLDER_\u003cfield>\"`.\n Pick this only when the transform was authored against\n the envelope shape on purpose.\n - |-\n Handlebars expressions inside `extract`\n (`{{record.field}}`, `{{dateFormat ... record.field}}`)\n ALWAYS reference the `record` root regardless of\n `inputContext` — only JSONPath anchoring flips.\n - |-\n In `envelope` mode the output is still a bare record: a\n `modify` transform returns the record with its mapped\n fields merged in (no `record` wrapper, no `job` or\n `settings` keys), and a `create` transform returns only\n the mapped fields.\n - |-\n Emit `inputContext` explicitly on new v2 transforms:\n unset is read as `record`, but stating it makes the\n extract-anchoring contract self-documenting and immune\n to a platform default change.\n - |-\n ## Allowed values\n\n - ``record`` (default — emitted when the field is absent\n / empty / undefined): the transform processor sees\n the FLAT record itself as its evaluation root.\n ``mappings[].extract`` JSONPath values MUST anchor\n directly at `` building-tools — Skillopedia ` with NO ``record.`` segment — for\n example ``\"$.orderId\"`` or\n ``\"$.customer.email\"``. PLACEHOLDER extracts use\n ``\"$.PLACEHOLDER_\u003cfield>\"``. This is the dominant\n Celigo idiom and the right choice for new transforms.\n\n - ``envelope``: the transform processor sees a wrapped\n object of the shape\n ``{record: {...}, job, testMode, settings, headers,\n queryParams}`` (plus ``fileMeta`` on file-provider\n steps; ``headers`` and ``queryParams`` sit beside\n ``record``) — the\n same runtime-context envelope Handlebars / SQL /\n GraphQL templates render against. ``mappings[].extract``\n JSONPath values for record fields MUST include the\n ``record.`` segment — for example ``\"$.record.orderId\"``\n or ``\"$.record.customer.email\"`` — while ``headers`` and\n ``queryParams`` are read beside it\n (``\"$.headers['X-Id']\"``). PLACEHOLDER extracts\n use ``\"$.record.PLACEHOLDER_\u003cfield>\"``. Pick this\n mode only when the export was authored against the\n envelope shape on purpose (typically because an\n upstream step intentionally feeds the wrapped\n runtime context through to the transform).\n\n ## Handlebars expressions are NOT affected\n\n Handlebars expressions inside ``extract``\n (``{{record.field}}``,\n ``{{dateFormat ... record.field}}``, etc.) ALWAYS\n reference the runtime context's ``record`` root\n regardless of ``inputContext`` — the Handlebars runtime\n always sees the wrapped envelope. Only JSONPath\n behavior flips with this field.\n\n ## Agent guidance\n\n Set this field explicitly on every new v2 transform\n the agent authors. Leaving it unset is interpreted as\n ``record`` by the platform, but emitting\n ``inputContext: record`` makes the contract\n self-documenting and prevents the transform from\n drifting into envelope mode if an upstream change ever\n changes the platform default. Read this value before\n writing ``mappings[].extract`` so the JSONPath form\n matches the runtime root.\n - |-\n ## fileMeta on file-provider steps\n\n File-provider exports and lookups populate a\n ``fileMeta`` envelope key describing each record's\n source file: ``fileName``, ``fileSize``, and\n ``lastModifiedTime`` (epoch milliseconds). Reach it\n as ``$.fileMeta.\u003cfield>`` in envelope mode, or as\n ``{{fileMeta.\u003cfield>}}`` from a Handlebars extract in\n either mode. The keys are exactly these three —\n lowercase variants such as ``filesize`` or\n ``filename`` resolve empty. The standalone transform\n processor substitutes sample fileMeta values when the\n posted data lacks them, so a resolving editor render\n is not proof the step provides fileMeta — only\n file-provider steps populate it at flow runtime.\n examples: [\"record\", \"envelope\"]\n script:\n type: object\n description: |\n Configuration for programmable script-based transformations. This object enables complex, custom\n transformation logic beyond what expression-based transformations can provide.\n\n **Usage context**\n\n This object is REQUIRED when `transform.type` is set to \"script\" and should not be configured\n otherwise. It provides a way to execute custom JavaScript code to transform data according to\n specialized business rules or complex algorithms.\n\n **Implementation approach**\n\n Script-based transformation works by:\n 1. Executing the specified function from the referenced script\n 2. Passing input data to the function\n 3. Using the function's return value as the transformed output\n\n **Common use cases**\n\n Script transformation is ideal for:\n - Complex business logic that can't be expressed through mappings\n - Algorithmic transformations requiring computation\n - Dynamic transformations based on external factors\n - Legacy system data format compatibility\n - Multi-stage processing with intermediate steps\n\n Only use script-based transformation when expression-based transformation is insufficient.\n Script transformation requires maintaining custom code, which adds complexity to the integration.\n properties:\n _scriptId:\n type: string\n description: |\n Reference to a predefined script resource containing the transformation logic.\n\n The referenced script should contain the function specified in the\n 'function' property. It must be a script in this account: any other id fails the save\n with 422 (\"Only a script owned by the user or a script shared by the Integration App can\n be used at transform.script._scriptId.\").\n format: objectid\n x-celigo-refModel: scripts\n examples: [\"60a2c4e6f321d800129a1a3c\", \"5f8d43a1b9e5a80011a35f2c\"]\n function:\n type: string\n description: |\n Name of the function within the script to execute for transformation. This function\n must exist in the script referenced by _scriptId.\n examples: [\"transformData\", \"convertRecord\", \"applyMapping\"]\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":21239,"content_sha256":"0f2a1bdcf3fd4c5a271d5fc97ba73ce416e3c9a25c3767407d9d0eac9738578f"},{"filename":"references/schemas/base.yml","content":"ToolBase:\n type: object\n description: Writable tool fields shared by the request and response schemas.\n properties:\n name:\n type: string\n minLength: 1\n maxLength: 100\n description: |\n Human-readable name for the tool.\n\n Displayed in the UI and used to identify the tool's purpose.\n examples: [\"Enrich Customer Data\", \"Validate Order\"]\n description:\n type: string\n maxLength: 5120\n description: |\n Optional detailed description of what the tool does.\n\n Use this to document the tool's purpose, expected inputs/outputs,\n and any special considerations.\n examples: [\"Validates and enriches incoming customer records by looking up account status and applying business rules.\"]\n _integrationId:\n type: string\n format: objectId\n x-celigo-refModel: integrations\n description: |\n Reference to the integration this tool belongs to.\n\n Every tool must be associated with an integration. The integration\n determines the scope and access controls for the tool.\n examples: [\"5f8d43a1b9e5a80011a35f2c\"]\n x-celigo-ai-guidance:\n - To move the tool to a different integration, PUT with a changed _integrationId; the platform registers the tool's step connections in the target integration on save.\n input:\n $ref: \"./input.yml#/Input\"\n output:\n $ref: \"./output.yml#/Output\"\n routers:\n type: array\n description: |\n Optional routers for conditional processing logic.\n\n Routers allow you to direct input data to different processing branches\n based on filter criteria or script logic. Tools only support\n \"first_matching_branch\" routing strategy.\n\n Branches can chain to other routers or use the special \"outputRouter\"\n terminal value to exit the tool.\n\n A tool saves without `routers`, but it can't run: `POST /v1/tools/{_id}/invoke`\n and a test run answer 400 `tool_empty_router` (\"Tool has no steps configured.\n Add at least one step before executing the tool.\"). A full PUT that omits\n `routers` removes the steps the same way. Put linear steps in one pass-through\n router with a single branch.\n items:\n $ref: \"./router.yml#/Router\"\n aiDescription:\n $ref: \"../common-schemas/ai-description.yml#/AIDescription\"\n draft:\n type: boolean\n description: |-\n When true, this tool is a draft that auto-deletes when its expiry passes\n (`draftExpiresAt` in the response). Set at creation; an update can clear the\n flag but never set it.\n x-celigo-ai-guidance:\n - |-\n POST with `draft: true` arms the draft TTL (the server computes `draftExpiresAt`).\n PUT with `draft: false` clears the flag and the expiry, promoting the draft to a\n permanent tool. PUT with `draft: true` on a non-draft tool is silently ignored,\n and a PUT that omits the field leaves an existing draft flag unchanged.\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":3015,"content_sha256":"a6c37a43e770890f2157d772c17187e8ba9cced99d6bd9651952b44ca61b8428"},{"filename":"references/schemas/input.yml","content":"Input:\n type: object\n description: |\n Configuration for the tool's input processing.\n\n Defines the expected input structure, optional transformations to apply\n before routing, and mock data for testing.\n properties:\n name:\n type: string\n maxLength: 200\n description: |\n Display name for the input configuration.\n examples: [\"Customer Record\", \"Order Payload\"]\n description:\n type: string\n maxLength: 10240\n description: |\n Description of the expected input data and its purpose.\n examples: [\"Incoming customer record from the CRM system\"]\n schema:\n type: object\n description: |\n JSON Schema describing the expected input data structure.\n\n Used for validation, documentation, and AI-assisted tooling.\n Must be a valid JSON Schema document. Leave `$schema` out or name draft-07: a\n `$schema` naming draft 2020-12 fails the save with 422 \"Invalid input.schema -\n no schema with key or ref ...\". An `$id` saves.\n\n Each invoke's input is validated against it. A missing `required` property\n refuses the call with 422 `tool_input_schema_validation_failed` (\"Tool input\n validation failed: must have required property '\u003cname>'\"), and a failed\n `pattern` with 422 \"Tool input validation failed: \u003cfield>: must match pattern\n ...\". A property made required later therefore breaks callers that don't send\n it.\n additionalProperties: true\n examples:\n - type: object\n properties:\n customerId:\n type: string\n name:\n type: string\n email:\n type: string\n format: email\n schemaMode:\n type: string\n enum: [manual, json]\n x-enumDescriptions:\n manual: The UI opens `schema` in the visual field-by-field editor\n json: The UI opens `schema` in the raw JSON editor\n description: |-\n Which editor the UI opens `schema` in; invocation input validates the same either way.\n Unset opens the visual editor, and an edit there rebuilds `schema` from the field list,\n keeping only `type`, `properties`, `required`, `items`, `description`, `enum`, `pattern`,\n `minLength`/`maxLength`, `minimum`/`maximum`, `exclusiveMinimum`/`exclusiveMaximum`,\n `multipleOf`, `minItems`/`maxItems` and a `\"null\"` type — `format`, `const`, `default`,\n `additionalProperties`, `$ref` and `allOf`/`anyOf`/`oneOf`/`if` are dropped without a\n prompt, and `integer` becomes `number`.\n x-celigo-ai-guidance:\n - |-\n Set `json` on any schema written as raw JSON so the UI opens the JSON editor and warns\n before a switch to the visual editor can drop keywords; keep `manual` on a schema a\n user built in the visual editor and stay inside the keywords it keeps.\n - |-\n A full PUT that omits this field clears it — resend the stored value on every update.\n PATCH rejects the path (\"/input/schemaMode is not a whitelisted property\"), and a value\n outside the enum fails the save with 422 `enum` on `input.schemaMode`.\n examples: [json]\n transform:\n $ref: \"../common-schemas/transform.yml#/Transform\"\n traceKeyTemplate:\n type: string\n maxLength: 1024\n x-celigo-agent:\n - handlebars\n x-celigo-handlebars-context: record\n description: |-\n Handlebars template evaluated against the invocation input (`record`) to produce\n the `traceKey` stored on each run-history entry, so an invocation can be found again\n with the `traceKeyPrefix` filter on `GET /v1/tools/{_id}/invocations`. Omit it and\n entries carry an empty `traceKey`.\n examples: [\"order-{{record.orderId}}\", \"{{record.customerId}}-{{record.requestId}}\"]\n x-celigo-ai-guidance:\n - |-\n The template must contain at least one `{{…}}` expression and must compile: a\n literal such as `order-static` fails the save with 422 on `input.traceKeyTemplate`\n (\"requires at least one double stash expression\"), and an unbalanced `{{record.id`\n fails with 422 \"Failed to compile handlebars template\". An empty string clears the\n field.\n debugUntil:\n type: string\n format: date-time\n description: |-\n Turns on debug capture for the Tool Input stage until this time: while it lies in\n the future, every record entering the tool is written to\n `GET /v1/tools/{_id}/tool_input/requests` with its input-schema outcome, in every\n execution context (standalone invoke, AI agent or MCP caller, flow or API). At\n most 3600 seconds ahead — a later value is rejected with `422` `update_not_allowed`\n and nothing is saved. Written by PUT or by `PATCH /v1/tools/{_id}` on\n `/input/debugUntil`; `remove` (or a past time) stops capture. Capture is not\n retroactive and does not change record retention.\n examples: [\"2026-08-25T12:30:00.000Z\"]\n mockInput:\n type: object\n description: |\n Sample input saved with the tool. `POST /v1/tools/{_id}/test/run` doesn't read it:\n send it as that call's `input`, or the run starts from an empty record. Maximum\n size: 1MB.\n additionalProperties: true\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":5351,"content_sha256":"0504d39fdfc385e3e8d6a6ca1fcc0b67c803cc40196f7de3f2829e3638960ae1"},{"filename":"references/schemas/output.yml","content":"Output:\n type: object\n description: |\n Configuration for the tool's output processing.\n\n Defines how the tool's results are mapped, transformed, and enriched\n before being returned. Supports field mappings, lookups for data\n enrichment, and custom script hooks for pre/post-mapping processing.\n properties:\n name:\n type: string\n maxLength: 200\n description: |\n Display name for the output configuration.\n examples: [\"Transformed Customer\", \"Order Response\"]\n description:\n type: string\n maxLength: 10240\n description: |\n Description of the output data and its purpose.\n examples: [\"Mapped and enriched customer record ready for the destination system\"]\n schema:\n type: object\n description: |\n JSON Schema describing the output data structure.\n\n Used for documentation and validation of the tool's output.\n Must be a valid JSON Schema document.\n\n The mapped output is validated against it. While the stored schema holds\n `additionalProperties: false`, `POST /v1/tools/{_id}/invoke` answers 422 \"Tool\n output validation failed: must NOT have additional properties\" when the output\n mappings write a field the schema doesn't list.\n additionalProperties: true\n examples:\n - type: object\n properties:\n id:\n type: string\n fullName:\n type: string\n status:\n type: string\n schemaMode:\n type: string\n enum: [manual, json]\n x-enumDescriptions:\n manual: The UI opens `schema` in the visual field-by-field editor\n json: The UI opens `schema` in the raw JSON editor\n description: |-\n Which editor the UI opens `schema` in; the output validates the same either way. Unset\n opens the visual editor, and an edit there rebuilds `schema` from the field list, keeping\n only `type`, `properties`, `required`, `items`, `description`, `enum`, `pattern`,\n `minLength`/`maxLength`, `minimum`/`maximum`, `exclusiveMinimum`/`exclusiveMaximum`,\n `multipleOf`, `minItems`/`maxItems` and a `\"null\"` type — `format`, `const`, `default`,\n `additionalProperties`, `$ref` and `allOf`/`anyOf`/`oneOf`/`if` are dropped without a\n prompt, and `integer` becomes `number`.\n x-celigo-ai-guidance:\n - |-\n Set `json` on any schema written as raw JSON so the UI opens the JSON editor and warns\n before a switch to the visual editor can drop keywords; keep `manual` on a schema a\n user built in the visual editor and stay inside the keywords it keeps.\n - |-\n A full PUT that omits this field clears it — resend the stored value on every update.\n PATCH rejects the path (\"/output/schemaMode is not a whitelisted property\"), and a\n value outside the enum fails the save with 422 `enum` on `output.schemaMode`.\n examples: [manual]\n mappings:\n description: |\n Field mappings to transform data into the output format.\n\n Maps data from processing results to the output structure.\n Uses Celigo's standard mapping format with extract/generate field paths —\n a flat array of mapping entries (each entry may recurse via its own\n nested ``mappings`` for object/array structures).\n allOf:\n - $ref: \"../common-schemas/mappings.yml#/Mappings\"\n lookups:\n type: array\n description: |\n Lookup tables for data enrichment during output processing.\n\n Static key-value mappings used to translate values (e.g., status codes,\n category names) during output generation.\n items:\n type: object\n properties:\n name:\n type: string\n description: |\n Name of the lookup, used to reference it from mappings.\n examples: [\"statusMapping\", \"regionLookup\"]\n map:\n type: object\n description: |\n Key-value mapping object. Keys are the input values and\n values are the corresponding output values.\n additionalProperties: true\n examples:\n - A: Active\n I: Inactive\n P: Pending\n default:\n type: string\n description: |\n Default value returned when the input key is not found in the map.\n examples: [\"Unknown\"]\n allowFailures:\n type: boolean\n description: |\n Whether to continue processing if the lookup fails to find a match\n and no default is provided.\n hooks:\n type: object\n description: |\n Custom script hooks for pre- and post-mapping processing.\n\n Allows running custom JavaScript functions before and after\n output mappings are applied.\n properties:\n preMap:\n type: object\n description: |\n Script to run before applying output mappings.\n\n Can modify the data before it is mapped to the output structure.\n properties:\n _scriptId:\n type: string\n format: objectId\n x-celigo-refModel: scripts\n description: Reference to the script resource\n examples: [\"60a2c4e6f321d800129a1a3c\"]\n function:\n type: string\n description: Function name to execute within the script\n examples: [\"preProcessOutput\", \"enrichData\"]\n postMap:\n type: object\n description: |\n Script to run after applying output mappings.\n\n Can modify the final output data after mappings are applied.\n properties:\n _scriptId:\n type: string\n format: objectId\n x-celigo-refModel: scripts\n description: Reference to the script resource\n examples: [\"60a2c4e6f321d800129a1a3c\"]\n function:\n type: string\n description: Function name to execute within the script\n examples: [\"postProcessOutput\", \"validateOutput\"]\n debugUntil:\n type: string\n format: date-time\n description: |-\n Turns on debug capture for the Tool Output stage until this time: while it lies in\n the future, every post-mapped record is written to\n `GET /v1/tools/{_id}/tool_output/requests` with its output-schema outcome, in every\n execution context (standalone invoke, AI agent or MCP caller, flow or API). At\n most 3600 seconds ahead — a later value is rejected with `422` `update_not_allowed`\n and nothing is saved. Written by PUT or by `PATCH /v1/tools/{_id}` on\n `/output/debugUntil`; `remove` (or a past time) stops capture. Capture is not\n retroactive and does not change record retention.\n examples: [\"2026-08-25T12:30:00.000Z\"]\n mockInput:\n type: object\n description: |\n Mock data for testing the tool's output processing.\n\n Provides sample data that would arrive from the routing/processing\n stage, used to test mapping and lookup logic. Maximum size: 1MB.\n additionalProperties: true\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":7250,"content_sha256":"439f2dfa19279791a529e1f836aea4280e48e7d5b5064f40c6df6aa4baf48baa"},{"filename":"references/schemas/request.yml","content":"Request:\n type: object\n description: |-\n Request schema for creating or updating a tool. Tools are reusable processing\n units that encapsulate input transformation, conditional routing, and output\n mapping logic within an integration.\n required:\n - name\n - _integrationId\n allOf:\n - $ref: \"./base.yml#/ToolBase\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":334,"content_sha256":"e1ac96e89d7dd15400500145056f1dc271bb40d9b7b1d5e308dd3d77564b4de4"},{"filename":"references/schemas/response.yml","content":"Tool:\n type: object\n required:\n - _id\n - name\n - _integrationId\n - createdAt\n - lastModified\n description: Tool object as returned by the API.\n allOf:\n - $ref: './base.yml#/ToolBase'\n - $ref: '../common-schemas/resource-response.yml#/ResourceResponse'\n - type: object\n properties:\n _sourceId:\n type: string\n format: objectId\n readOnly: true\n description: Origin resource ID when this tool was created by cloning or installing a template.\n examples: [\"69afbdb19c78a72fc05b1b8b\"]\n sandbox:\n type: boolean\n deprecated: true\n readOnly: true\n description: Whether this is a sandbox tool for testing.\n examples: [false]\n draftExpiresAt:\n type: string\n format: date-time\n readOnly: true\n description: |-\n Timestamp when a draft tool auto-deletes. Server-computed when `draft` is set at\n creation.\n examples: [\"2026-01-15T09:30:00.000Z\"]\n x-celigo-ai-guidance:\n - |-\n Contains the complete tool configuration including metadata, input/output\n settings, routing logic, and AI-generated descriptions.\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":1216,"content_sha256":"569587fd285eaceb38c45014037dfebf1a9d43b8f8d85ba1f58a1c157b5e784d"},{"filename":"references/schemas/router.yml","content":"Router:\n type: object\n description: |\n Configuration for conditional routing within a tool.\n\n Routers evaluate input data and direct it to different processing branches\n based on criteria. This enables complex business logic and conditional\n processing within the tool.\n\n Unlike flows, tools only support \"first_matching_branch\" routing strategy.\n Branches can chain to other routers or use the special \"outputRouter\"\n terminal sink to exit the tool and return results.\n\n A router comes in two shapes, told apart by whether `routeRecordsTo` and\n `routeRecordsUsing` are set: a **branching router** sets both and has two or\n more branches, each taking the inputs its `inputFilter` (or the router's\n `script`) matches — the node you see in the Tool builder; a **pass-through\n router** sets neither and has exactly one branch that every input goes\n straight through — not drawn, it holds the steps before the first branching\n router and merges branches back into shared steps (branches rejoin only by\n pointing `nextRouterId` at the same router).\n x-celigo-ai-guidance:\n - |-\n Set `routeRecordsTo` and `routeRecordsUsing` together or omit both — a router with\n only one of them fails the save with 422 `invalid_flow_router_config`.\n - |-\n Every branch needs at least one entry in `pageProcessors` before the tool can run:\n `POST /v1/tools/{_id}/invoke` on a tool whose branches are empty returns 400\n `tool_empty_router`.\n properties:\n id:\n type: string\n maxLength: 36\n pattern: '^[A-Za-z0-9_-]+

<!-- TIER:1 -- Building Tools A tool is Celigo's first-class reusable building block . It encapsulates logic -- lookups, imports, transforms, branching -- behind a defined input and output contract. Build it once, use it everywhere: from Flows, APIs, AI Agents, MCP Servers, and other Tools. Tool Concepts Why tools exist: Without tools, users build the same lookup-transform-import patterns repeatedly across Flows, APIs, and Agents. Tools solve this by providing a governed, composable abstraction: one definition, many consumers, consistent behavior. When to build a tool: - You're building an MC…

\n description: |\n Unique identifier for this router within the tool.\n\n Used to reference this router from other routers' branch `nextRouterId`.\n Use at most 36 letters, digits, underscores and hyphens: a longer id fails\n the save with 422 (\"Router id is missing or contains more than 36 characters\n or invalid characters. Please correct the router id.\"), as on flow routers.\n examples: [\"router_001\", \"validation_router\"]\n name:\n type: string\n maxLength: 300\n description: |\n Human-readable name for the router.\n examples: [\"Customer Type Router\", \"Validation Router\"]\n routeRecordsTo:\n type: string\n enum: [\"first_matching_branch\"]\n x-enumDescriptions:\n first_matching_branch: Routes the input to the first branch whose criteria match.\n description: |\n Routing strategy. Tools only support \"first_matching_branch\",\n which routes to the first branch whose criteria match the input.\n Set together with `routeRecordsUsing` on a branching router; leave\n both unset on a one-branch pass-through router.\n examples: [\"first_matching_branch\"]\n x-celigo-ai-guidance:\n - Set this and `routeRecordsUsing` together on every router that decides between branches; setting only one of the two is rejected with 422 `invalid_flow_router_config`.\n - Leave both unset on a one-branch pass-through router (the steps before the first branching router, or a merge point). Setting them there still runs, but the builder shows a router node with one branch and no rules. A router with neither field and more than one branch is rejected with 422 `invalid_flow_router_branch`.\n routeRecordsUsing:\n type: string\n enum: [\"input_filters\", \"script\"]\n x-enumDescriptions:\n input_filters: Evaluate routing criteria using declarative filter expressions.\n script: Evaluate routing criteria using a custom JavaScript function.\n description: |\n How a branching router decides which branch an input matches. Set together\n with `routeRecordsTo`; leave both unset on a one-branch pass-through router.\n examples: [\"input_filters\"]\n x-celigo-ai-guidance:\n - Set this and `routeRecordsTo` together on every router that decides between branches, and leave both unset on a one-branch pass-through router; the rejections for a half-set pair or a multi-branch pass-through are listed on `routeRecordsTo`.\n script:\n type: object\n description: |\n Script configuration when routeRecordsUsing is \"script\".\n\n The function should return the name of the branch to route to.\n properties:\n _scriptId:\n type: string\n format: objectId\n x-celigo-refModel: scripts\n description: Reference to the script resource\n examples: [\"60a2c4e6f321d800129a1a3c\"]\n function:\n type: string\n description: Function name that returns the branch name\n examples: [\"routeInput\", \"determineBranch\"]\n branches:\n type: array\n description: |\n List of branches defining different processing paths.\n\n Each branch has optional filter criteria and a set of processing steps.\n Records are evaluated against branch criteria in order; the first\n matching branch is selected.\n items:\n type: object\n properties:\n name:\n type: string\n maxLength: 300\n description: |\n Name of this branch.\n examples: [\"Premium Customers\", \"Default Processing\"]\n description:\n type: string\n maxLength: 10240\n description: |\n Description of when and why this branch is selected.\n examples: [\"Selected when the customer is a premium account.\"]\n branchId:\n type: string\n description: |\n Stable identifier for this branch within the tool, generated by\n the builder. Used to reference the branch independently of its\n position in the branches array (e.g., from step requests).\n examples: [\"VD0S9xQPLAH\"]\n inputFilter:\n type: object\n description: |\n Filter criteria to determine if this branch should be selected.\n\n Uses Celigo's expression-based filter format. Send `version` and `rules`\n directly on `inputFilter` — not inside the `{type: \"expression\", expression: {...}}`\n wrapper that export and import filters use. A wrapped filter is saved without an\n error, but the branch is stored with no `inputFilter` and takes every input.\n x-celigo-ai-guidance:\n - Read the tool back after saving a branching router and check that each filtered branch still has its `inputFilter`; a filter sent in the export-filter wrapper is dropped on save without an error.\n properties:\n version:\n type: string\n enum: [\"1\"]\n x-enumDescriptions:\n \"1\": Version 1 of the Celigo expression-based filter format.\n description: Filter version\n rules:\n type: array\n description: |\n Filter rules in Celigo expression-based filter format.\n\n Array-based DSL where the first element is an operator (e.g., \"equals\", \"and\", \"or\"),\n followed by operands which can be nested expressions.\n items: {}\n examples:\n - [\"equals\", [\"string\", [\"extract\", \"customerType\"]], \"premium\"]\n - [\"and\", [\"notempty\", [\"string\", [\"extract\", \"orderId\"]]], [\"greaterthan\", [\"number\", [\"extract\", \"amount\"]], 1000]]\n nextRouterId:\n type: string\n description: |\n Identifier of the next router to chain to after this branch completes.\n\n Use \"outputRouter\" as a special terminal value to exit the tool\n and return the processing results.\n\n Every branch needs one. A branch without it is saved, but every invoke is\n then refused with 422 and every test run with 400 `tool_dangling_branch`\n (\"One or more branches are not merged back. Review the setup to ensure all\n branches converge.\"), whichever branch the input takes. An id that names no\n router fails the save with 422 \"Invalid nextRouterId is defined on the branch.\n Please correct the branch configuration.\"\n examples: [\"router_002\", \"outputRouter\"]\n pageProcessors:\n type: array\n description: |\n Processing steps to execute in this branch.\n\n Each processor references an export (lookup) or import resource\n for data retrieval or submission. An export is a step at most once in a\n tool: using it again, on another branch or in the same one, fails the save\n with 422 \"The export \u003cid> is already included as a pageProcessor in your\n flow, and cannot be added again.\"\n items:\n type: object\n properties:\n type:\n type: string\n enum: [\"export\", \"import\"]\n x-enumDescriptions:\n export: Retrieves data from an external system (lookup).\n import: Sends data to an external system.\n description: |\n Type of processor.\n\n - **export**: Retrieves data from an external system (lookup)\n - **import**: Sends data to an external system\n _exportId:\n type: string\n format: objectId\n x-celigo-refModel: exports\n description: Export resource reference (when type is \"export\")\n examples: [\"60a2c4e6f321d800129a1a3c\"]\n _importId:\n type: string\n format: objectId\n x-celigo-refModel: imports\n description: Import resource reference (when type is \"import\")\n examples: [\"60a2c4e6f321d800129a1a3c\"]\n proceedOnFailure:\n type: boolean\n description: |-\n When true, the later steps and the output stage still run after this\n processor fails; when false, they're skipped. Either way a direct\n `POST /v1/tools/{_id}/invoke` answers 422 with the failed step's error,\n without the output.\n setupInProgress:\n type: boolean\n description: |\n When true, the processor's configuration is still being\n set up in the UI and the step is not yet runnable.\n responseMapping:\n type: object\n description: |\n Merges fields from this processor's response back onto the\n in-flight record so later processors and the tool's output\n can read them. Extracts do NOT read the raw application\n response — they evaluate against the platform's canonical\n per-record envelope: for lookups (`type: \"export\"`) that is\n `{\"statusCode\", \"data\": [\u003cresult records>], \"errors\"}`\n where `data` holds the records AFTER the export's own\n `transform` and output `filter`, so paths must start from\n `data` and name transformed fields by their transformed\n name (e.g. `data[0].name`) — a lookup result field\n written without the `data` prefix resolves to nothing;\n for imports it is `{\"id\", \"statusCode\", \"ignored\",\n \"_json\", ...\u003cbody fields flattened>}`, so use `id`,\n `_json.\u003cpath>`, or the body field itself at the root\n (`orderNumber` and `_json.orderNumber` read the same\n value). An extract may be a Handlebars expression over\n the same envelope (`{{data.0.name}}`,\n `{{#if data.length}}found{{/if}}`; dot indexes only; a\n string result); there is no `record` object in this\n context, so `{{record.…}}` renders nothing.\n properties:\n fields:\n type: array\n description: Simple field-level mappings\n items:\n type: object\n properties:\n extract:\n type: string\n x-celigo-agent:\n - handlebars\n description: |\n Path within the canonical response envelope to\n copy the value from (`data[0].x` / `data.0.x`\n for lookups; `id`, `_json.\u003cpath>` or the\n flattened body field `\u003cpath>` for imports), or\n a Handlebars expression over that envelope (dot\n indexes inside `{{ }}`, never a `record.` root).\n examples: [\"data[0].customer.name\", \"data.0.fields.Product Name\", \"id\", \"{{#if data.length}}found{{else}}missing{{/if}}\"]\n generate:\n type: string\n description: |\n Field path on the in-flight record where the\n extracted value is stored (dot notation for\n nesting).\n examples: [\"orderId\", \"supplierDetails.supplierName\"]\n lists:\n type: array\n description: List-level mappings for array data\n items:\n type: object\n properties:\n generate:\n type: string\n description: Target list path\n examples: [\"lineItems\"]\n fields:\n type: array\n description: Field-level mappings applied to each item in the list.\n items:\n type: object\n properties:\n extract:\n type: string\n description: Source field path\n examples: [\"sku\"]\n generate:\n type: string\n description: Target field path\n examples: [\"itemCode\"]\n hooks:\n type: object\n description: Custom scripts for processing\n properties:\n postResponseMap:\n type: object\n description: Script to run after response mapping\n properties:\n _scriptId:\n type: string\n format: objectId\n x-celigo-refModel: scripts\n description: Reference to the script resource\n examples: [\"60a2c4e6f321d800129a1a3c\"]\n function:\n type: string\n description: Function name to execute\n examples: [\"postProcessResponse\"]\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":15576,"content_sha256":"54769c2659bf1734b629700be5cec0174b67da4de5b6b0c8b124519d96c17c62"}],"content_json":{"type":"doc","content":[{"type":"paragraph","content":[{"text":"\u003c!-- TIER:1 -->","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Building Tools","type":"text"}]},{"type":"paragraph","content":[{"text":"A tool is Celigo's ","type":"text"},{"text":"first-class reusable building block","type":"text","marks":[{"type":"strong"}]},{"text":". It encapsulates logic -- lookups, imports, transforms, branching -- behind a defined input and output contract. Build it once, use it everywhere: from Flows, APIs, AI Agents, MCP Servers, and other Tools.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Tool Concepts","type":"text"}]},{"type":"paragraph","content":[{"text":"Why tools exist:","type":"text","marks":[{"type":"strong"}]},{"text":" Without tools, users build the same lookup-transform-import patterns repeatedly across Flows, APIs, and Agents. Tools solve this by providing a governed, composable abstraction: one definition, many consumers, consistent behavior.","type":"text"}]},{"type":"paragraph","content":[{"text":"When to build a tool:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"You're building an ","type":"text"},{"text":"MCP server","type":"text","marks":[{"type":"strong"}]},{"text":" -- MCP servers expose tools as endpoints; every piece of logic an MCP server offers must be a tool","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The same logic is needed by ","type":"text"},{"text":"2+ consumers","type":"text","marks":[{"type":"strong"}]},{"text":" (flows, APIs, agents, MCP servers) -- build once, call everywhere","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"You want ","type":"text"},{"text":"connection flexibility","type":"text","marks":[{"type":"strong"}]},{"text":" -- callers can pass different connections to the same tool definition","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"You're ","type":"text"},{"text":"composing smaller pieces","type":"text","marks":[{"type":"strong"}]},{"text":" -- tools can call other tools for nested orchestration","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"When NOT to build a tool:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The logic is ","type":"text"},{"text":"specific to one flow or API and won't be reused","type":"text","marks":[{"type":"strong"}]},{"text":" -- inline it as a lookup/import processor directly","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"You need ","type":"text"},{"text":"abstract/instance templating","type":"text","marks":[{"type":"strong"}]},{"text":" for multi-tenant patterns -- use abstract flows","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Tool vs API:","type":"text","marks":[{"type":"strong"}]},{"text":" the split is the contract callers get. A ","type":"text"},{"text":"tool","type":"text","marks":[{"type":"strong"}]},{"text":" is called by flow steps, AI agents, APIs and other tools, and outside callers can reach it too: a caller authenticated to the Celigo REST API can run it directly through its invoke operation (input plus connection bindings; the output comes back synchronously), and an MCP server publishes tools to outside AI clients. Put an ","type":"text"},{"text":"API","type":"text","marks":[{"type":"strong"}]},{"text":" in front only when callers need their own URL, method and status codes (a partner system, a customer-facing app) -- the tool stays available to its other consumers at the same time.","type":"text"}]},{"type":"paragraph","content":[{"text":"Tool vs flow:","type":"text","marks":[{"type":"strong"}]},{"text":" tools don't start themselves -- no ","type":"text"},{"text":"schedule","type":"text","marks":[{"type":"code_inline"}]},{"text":", no listeners, no flow-level runtime controls, and no abstract/instance layer (a tool is already the unit of reuse; per-environment variation is handled by the consumer-bound connection model). A step's own ","type":"text"},{"text":"proceedOnFailure","type":"text","marks":[{"type":"code_inline"}]},{"text":" is set on its page-processor entry, as in a flow (","type":"text"},{"text":"step 7","type":"text","marks":[{"type":"link","attrs":{"href":"#7-wire-page-processors-into-branches","title":null}}]},{"text":"). \"Every night, do X\" or \"when a webhook arrives, do Y\" is a flow -- the tool may be the thing the flow ","type":"text"},{"text":"does","type":"text","marks":[{"type":"em"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Architecture:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Tool\n+-- input -- JSON Schema defining what callers send\n+-- routers[] -- processing pipeline\n| +-- branches[]\n| +-- pageProcessors[] -- lookups (exports), imports, transfers\n+-- output -- schema, mappings, lookups, hooks defining what the tool returns","type":"text"}]},{"type":"paragraph","content":[{"text":"Routers hold branches, branches hold page processors. Use multiple branches when different inputs need different processing paths. Chain routers via ","type":"text"},{"text":"nextRouterId","type":"text","marks":[{"type":"code_inline"}]},{"text":" for sequential processing stages. The special ","type":"text"},{"text":"nextRouterId: \"outputRouter\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" exits the tool and returns results.","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Tool Execution Pipeline","type":"text"}]},{"type":"paragraph","content":[{"text":"When a tool is invoked:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Input received","type":"text","marks":[{"type":"strong"}]},{"text":" -- input data validated against the tool's JSON Schema (","type":"text"},{"text":"input","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Router evaluation","type":"text","marks":[{"type":"strong"}]},{"text":" -- ","type":"text"},{"text":"routeRecordsUsing","type":"text","marks":[{"type":"code_inline"}]},{"text":" evaluates branch conditions (if multiple branches exist)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Branch selection","type":"text","marks":[{"type":"strong"}]},{"text":" -- matching branch processes the input","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Page processors","type":"text","marks":[{"type":"strong"}]},{"text":" -- each processor executes sequentially (export lookups, import writes)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Response mapping","type":"text","marks":[{"type":"strong"}]},{"text":" -- ","type":"text"},{"text":"responseMapping","type":"text","marks":[{"type":"code_inline"}]},{"text":" on each processor carries data to the next processor","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Output mapping","type":"text","marks":[{"type":"strong"}]},{"text":" -- final output mapped according to the tool's ","type":"text"},{"text":"output","type":"text","marks":[{"type":"code_inline"}]},{"text":" configuration (schema, mappings, lookups, hooks)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Return","type":"text","marks":[{"type":"strong"}]},{"text":" -- output returned to the caller (flow, API, AI agent, MCP server)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Execution mode depends on the caller:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Called from a ","type":"text"},{"text":"Flow","type":"text","marks":[{"type":"strong"}]},{"text":" -- runs in flow mode (batch-oriented, run console, error management)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Called from an ","type":"text"},{"text":"API","type":"text","marks":[{"type":"strong"}]},{"text":" -- runs in API mode (single request/response)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Called from an ","type":"text"},{"text":"AI Agent","type":"text","marks":[{"type":"strong"}]},{"text":" -- runs with agent context; agent maps connections to the tool","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Called from an ","type":"text"},{"text":"MCP Server","type":"text","marks":[{"type":"strong"}]},{"text":" -- exposed as an MCP tool endpoint for external AI clients","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Called from another ","type":"text"},{"text":"Tool","type":"text","marks":[{"type":"strong"}]},{"text":" -- nested execution within the parent tool's context","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"In a flow, an API or another tool, the tool runs as a ","type":"text"},{"text":"ToolImport","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" step: an import with ","type":"text"},{"text":"\"adaptorType\": \"ToolImport\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"\"tool\": {\"_toolId\": \"\u003ctool id>\", \"overrides\": {\"connections\": [...]}}","type":"text","marks":[{"type":"code_inline"}]},{"text":" and no ","type":"text"},{"text":"_connectionId","type":"text","marks":[{"type":"code_inline"}]},{"text":", which the parent's page processor references like any import (","type":"text"},{"text":"{\"type\": \"import\", \"_importId\": \"\u003cwrapper id>\"}","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Change the tool through ","type":"text"},{"text":"tools","type":"text","marks":[{"type":"code_inline"}]},{"text":" and the wrapper through ","type":"text"},{"text":"imports","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Connection model:","type":"text","marks":[{"type":"strong"}]},{"text":" Always bring-your-own-keys. The caller (flow, API, agent, MCP server) maps connections to the tool at configuration time. MCP Server overrides can swap connections per-server without modifying the tool.","type":"text"}]},{"type":"paragraph","content":[{"text":"Build order:","type":"text","marks":[{"type":"strong"}]},{"text":" Connection --> Export + Import --> Tool --> (consumer: Flow / API / Agent / MCP Server)","type":"text"}]},{"type":"paragraph","content":[{"text":"Concerns beyond the build steps:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Response mapping","type":"text","marks":[{"type":"strong"}]},{"text":" -- extract fields from processor responses back into the record. Configured on ","type":"text"},{"text":"pageProcessors[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" entries within branches as ","type":"text"},{"text":"{\"fields\": [{\"extract\": ..., \"generate\": ...}], \"lists\": []}","type":"text","marks":[{"type":"code_inline"}]},{"text":", but planned when building the processors. For lookups the response has ","type":"text"},{"text":"data[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"errors[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" (use ","type":"text"},{"text":"data[0].fieldName","type":"text","marks":[{"type":"code_inline"}]},{"text":" for one field of the first result, or ","type":"text"},{"text":"data[0]","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the whole record), and ","type":"text"},{"text":"data","type":"text","marks":[{"type":"code_inline"}]},{"text":" holds the records after the export's own transform and output filter, so use the transformed field names; for imports use ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the created record's ID and ","type":"text"},{"text":"_json.fieldName","type":"text","marks":[{"type":"code_inline"}]},{"text":" for response-body fields (the body's fields are also at the root, so ","type":"text"},{"text":"orderNumber","type":"text","marks":[{"type":"code_inline"}]},{"text":" reads the same as ","type":"text"},{"text":"_json.orderNumber","type":"text","marks":[{"type":"code_inline"}]},{"text":"). An extract can be Handlebars over this envelope (","type":"text"},{"text":"{{data.0.name}}","type":"text","marks":[{"type":"code_inline"}]},{"text":"), which has no ","type":"text"},{"text":"record","type":"text","marks":[{"type":"code_inline"}]},{"text":": ","type":"text"},{"text":"{{record.x}}","type":"text","marks":[{"type":"code_inline"}]},{"text":" renders nothing","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"postResponseMap hook","type":"text","marks":[{"type":"strong"}]},{"text":" -- JavaScript processing after response mapping. Also on ","type":"text"},{"text":"pageProcessors[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" entries, but planned when building the processors","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"The Two Flavors of Lookup","type":"text"}]},{"type":"paragraph","content":[{"text":"\"Lookup\" refers to two different things in a tool. They live in different places and solve different problems:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Branch page-processor lookups","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"pageProcessors[].type: \"export\"","type":"text","marks":[{"type":"code_inline"}]},{"text":") -- work-doing lookups inside a router branch that call an external system at runtime (\"look up the customer in NetSuite by email\"). The data isn't in the tool yet; the export fetches it, and a ","type":"text"},{"text":"responseMapping","type":"text","marks":[{"type":"code_inline"}]},{"text":" pulls fields from the response onto the record so downstream branches and the output can see them. This is the same lookup primitive flows use -- same export resource, same response mapping, same ","type":"text"},{"text":"postResponseMap","type":"text","marks":[{"type":"code_inline"}]},{"text":" hook.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Output static lookup tables","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"output.lookups[]","type":"text","marks":[{"type":"code_inline"}]},{"text":") -- declarative value-translation tables on the output stage. They reach no external system; they are fixed key/value maps defined inline (e.g. a map translating ","type":"text"},{"text":"A","type":"text","marks":[{"type":"code_inline"}]},{"text":" to ","type":"text"},{"text":"Active","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"I","type":"text","marks":[{"type":"code_inline"}]},{"text":" to ","type":"text"},{"text":"Inactive","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"P","type":"text","marks":[{"type":"code_inline"}]},{"text":" to ","type":"text"},{"text":"Pending","type":"text","marks":[{"type":"code_inline"}]},{"text":", with a ","type":"text"},{"text":"default","type":"text","marks":[{"type":"code_inline"}]},{"text":"). An output ","type":"text"},{"text":"mappings[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" entry references a table by its ","type":"text"},{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":" (via ","type":"text"},{"text":"lookupName","type":"text","marks":[{"type":"code_inline"}]},{"text":") to translate a field value during output assembly.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Rule of thumb: external system, runtime call --> branch page-processor lookup. Translate one value into another via a fixed table --> output static lookup.","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Consumer-Bound Connections","type":"text"}]},{"type":"paragraph","content":[{"text":"A tool never pins connections at design time -- the key difference from flows and APIs:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A flow or API pins each of its own steps to a specific connection at build time","type":"text","marks":[{"type":"strong"}]},{"text":" -- the connection IDs are baked into the resource.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A tool's lookups and imports declare the connections they need","type":"text","marks":[{"type":"strong"}]},{"text":", and the consumer supplies the actual connection at bind time -- when the tool is added as a flow step, attached to an AI agent, exposed behind an API endpoint, or embedded in another tool.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"The tool definition stays unchanged across every binding. The same tool can be bound with a sandbox NetSuite connection from one consumer and a production NetSuite connection from another, without forking. Mechanically, the tool's page processors reference underlying export and import resources that carry the connection; binding selects which connection records those resources use in that context. Because of this, knowing which connections a tool requires is part of its design -- those connections must already exist in the consumer's account before the tool can be bound and run there.","type":"text"}]},{"type":"paragraph","content":[{"text":"Binding isn't optional. A direct invoke and a tool step (a ","type":"text"},{"text":"ToolImport","type":"text","marks":[{"type":"code_inline"}]},{"text":") must list every connection the tool's steps use as ","type":"text"},{"text":"{\"_abstractId\": \u003cauthored id>, \"_id\": \u003cconnection to run with>}","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- in the invoke body's ","type":"text"},{"text":"overrides.connections","type":"text","marks":[{"type":"code_inline"}]},{"text":", or in the import's ","type":"text"},{"text":"tool.overrides.connections","type":"text","marks":[{"type":"code_inline"}]},{"text":". The same id twice keeps the authored connection, and leaving one out fails with ","type":"text"},{"text":"connection_not_overridden","type":"text","marks":[{"type":"code_inline"}]},{"text":" (\"Missing connection overrides\"). The connection to run with goes in ","type":"text"},{"text":"_id","type":"text","marks":[{"type":"code_inline"}]},{"text":": a binding that names it ","type":"text"},{"text":"_connectionId","type":"text","marks":[{"type":"code_inline"}]},{"text":", or gives only ","type":"text"},{"text":"_abstractId","type":"text","marks":[{"type":"code_inline"}]},{"text":", is refused with 422 \"Invalid override at overrides.connections[0]. Connection override must include at least one of: _id, ...\". ","type":"text"},{"text":"celigo tools connections \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":" lists the connections to bind. An MCP server binds per step instead; see ","type":"text"},{"text":"building-mcp-servers > How to Build an MCP Server","type":"text","marks":[{"type":"link","attrs":{"href":"../building-mcp-servers/SKILL.md#how-to-build-an-mcp-server","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Reference","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Decision Matrix","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"You need to...","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Build a tool?","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Instead use","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Reuse logic across 2+ flows/APIs/agents","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Expose logic via MCP server","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Allow callers to swap connections","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Nest orchestration (tool calls tool)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"One-off logic for a single flow","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Inline lookup/import in flow","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Multi-tenant templating","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Abstract/instance flows","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Minimum Required Fields","type":"text"}]},{"type":"paragraph","content":[{"text":"Every tool needs at minimum: ","type":"text"},{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"_integrationId","type":"text","marks":[{"type":"code_inline"}]},{"text":" (a create without it fails with 422 \"Expected field: _integrationId to be present.\"), an ","type":"text"},{"text":"input.schema","type":"text","marks":[{"type":"code_inline"}]},{"text":", and a router whose branch has a step. A tool without steps saves, but every invoke and test run is refused (","type":"text"},{"text":"step 6","type":"text","marks":[{"type":"link","attrs":{"href":"#6-configure-routing-if-needed","title":null}}]},{"text":").","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Which Schemas to Read","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Always: ","type":"text"},{"text":"request.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/request.yml","title":null}}]},{"text":" (base fields for create/update)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Input config: ","type":"text"},{"text":"input.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/input.yml","title":null}}]},{"text":" (schema, transform, mockInput)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Output config: ","type":"text"},{"text":"output.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/output.yml","title":null}}]},{"text":" (mappings, lookups, hooks)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pipeline config: ","type":"text"},{"text":"router.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/router.yml","title":null}}]},{"text":" (routing strategy, branches, page processors, response mapping)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Response shape: ","type":"text"},{"text":"response.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/response.yml","title":null}}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Schema Index","type":"text"}]},{"type":"paragraph","content":[{"text":"All schemas are in ","type":"text"},{"text":"references/schemas/","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/","title":null}}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Base fields (create/update):","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"request.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/request.yml","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Response shape:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"response.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/response.yml","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Input configuration:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"input.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/input.yml","title":null}}]},{"text":" -- schema, transform, mockInput","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Output configuration:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"output.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/output.yml","title":null}}]},{"text":" -- mappings, lookups, hooks","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Router and branch configuration:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"router.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/router.yml","title":null}}]},{"text":" -- routing strategy, branches, page processors, response mapping","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Related Skills","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"configuring-exports > Quick Reference","type":"text","marks":[{"type":"link","attrs":{"href":"../configuring-exports/SKILL.md#quick-reference","title":null}}]},{"text":" -- building lookup exports used as steps in the tool pipeline","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"configuring-imports > Quick Reference","type":"text","marks":[{"type":"link","attrs":{"href":"../configuring-imports/SKILL.md#quick-reference","title":null}}]},{"text":" -- building imports used as action steps in the tool pipeline","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"building-flows > How to Build a Flow","type":"text","marks":[{"type":"link","attrs":{"href":"../building-flows/SKILL.md#how-to-build-a-flow","title":null}}]},{"text":" -- wiring tools into flow pipelines","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"building-apis > Quick Reference","type":"text","marks":[{"type":"link","attrs":{"href":"../building-apis/SKILL.md#quick-reference","title":null}}]},{"text":" -- exposing tools via API endpoints","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"writing-scripts > Quick Reference","type":"text","marks":[{"type":"link","attrs":{"href":"../writing-scripts/SKILL.md#quick-reference","title":null}}]},{"text":" -- script hooks on tool page processors","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"writing-handlebars > Quick Reference","type":"text","marks":[{"type":"link","attrs":{"href":"../writing-handlebars/SKILL.md#quick-reference","title":null}}]},{"text":" -- dynamic expressions in request bodies, URIs, and field mappings","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"configuring-filters > Quick Reference","type":"text","marks":[{"type":"link","attrs":{"href":"../configuring-filters/SKILL.md#quick-reference","title":null}}]},{"text":" -- input filters on tool steps and router branches","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"\u003c!-- TIER:2 -->","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"How to Build a Tool","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. Determine the tool's purpose","type":"text"}]},{"type":"paragraph","content":[{"text":"What should this tool do when called? Define the inputs it expects and the processing steps it needs.","type":"text"}]},{"type":"paragraph","content":[{"text":"Write its ","type":"text"},{"text":"description","type":"text","marks":[{"type":"code_inline"}]},{"text":" (up to 5,120 characters) for the model that decides when to call it: what it does now, what it takes and what it returns. An MCP server serves it verbatim as the tool's description, with the Tool's ","type":"text"},{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":" as the title and its schemas as the input and output, and an AI agent step reads the same ","type":"text"},{"text":"description","type":"text","marks":[{"type":"code_inline"}]},{"text":" and input schema.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Identify the integration","type":"text"}]},{"type":"paragraph","content":[{"text":"Every tool belongs to an integration. Find or create the integration first.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"celigo integrations list","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. Check for existing patterns","type":"text"}]},{"type":"paragraph","content":[{"text":"Before building from scratch, look at what already exists:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Search your account (fast, uses local index)\nceligo account search \"\u003ckeyword>\"\n\n# Show what an existing tool uses -- tools aren't in the local account index\nceligo tools connections \u003cid>\nceligo tools get \u003cid>\n\n# Find orphaned resources that could be reused\nceligo account lint\n\n# Check if similar tools already exist\nceligo tools list\n\n# Search marketplace for pre-built integration templates\nceligo templates marketplace","type":"text"}]},{"type":"paragraph","content":[{"text":"The account index auto-refreshes when stale (older than 15 minutes by default). Force a fresh snapshot with ","type":"text"},{"text":"celigo account snapshot","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Existing tools in the account are the best reference -- they show proven patterns for that specific customer's setup. Marketplace templates may provide a complete pre-built integration you can install rather than building from scratch.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4. Build the connections, exports, and imports","type":"text"}]},{"type":"paragraph","content":[{"text":"Tools reference exports and imports as page processors in router branches. Build bottom-up: connections first, then exports and imports that use those connections, then the tool that wires them together. See ","type":"text"},{"text":"configuring-exports","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"configuring-imports","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5. Define the input schema","type":"text"}]},{"type":"paragraph","content":[{"text":"The input schema is a JSON Schema object describing what data the tool accepts. For MCP compatibility, the root schema must have ","type":"text"},{"text":"type: \"object\"","type":"text","marks":[{"type":"code_inline"}]},{"text":". Every call is checked against it: a missing ","type":"text"},{"text":"required","type":"text","marks":[{"type":"code_inline"}]},{"text":" property or a failed ","type":"text"},{"text":"pattern","type":"text","marks":[{"type":"code_inline"}]},{"text":" refuses the call with 422 \"Tool input validation failed: ...\", so a new required property breaks callers that don't send it. Leave ","type":"text"},{"text":"$schema","type":"text","marks":[{"type":"code_inline"}]},{"text":" out -- a draft 2020-12 ","type":"text"},{"text":"$schema","type":"text","marks":[{"type":"code_inline"}]},{"text":" fails the save with 422 \"Invalid input.schema - no schema with key or ref ...\" -- and set ","type":"text"},{"text":"schemaMode: \"json\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" on a schema you write as JSON (Gotcha 12).","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"6. Configure routing (if needed)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No routing","type":"text","marks":[{"type":"strong"}]},{"text":" -- all inputs processed the same way: one pass-through router with one branch and no routing fields (Gotcha 4). Don't leave ","type":"text"},{"text":"routers","type":"text","marks":[{"type":"code_inline"}]},{"text":" out: a tool with no steps, or with a branch that has none, is refused on every invoke and test run with 400 \"Tool has no steps configured. Add at least one step before executing the tool.\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Filter-based routing","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"routeRecordsUsing: \"input_filters\"","type":"text","marks":[{"type":"code_inline"}]},{"text":") -- declarative expression rules on each branch, as ","type":"text"},{"text":"\"inputFilter\": {\"version\": \"1\", \"rules\": [...]}","type":"text","marks":[{"type":"code_inline"}]},{"text":" (Gotcha 15)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Script-based routing","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"routeRecordsUsing: \"script\"","type":"text","marks":[{"type":"code_inline"}]},{"text":") -- a custom JavaScript ","type":"text"},{"text":"branching","type":"text","marks":[{"type":"code_inline"}]},{"text":" function returns an array of branch indices (zero-based positions in ","type":"text"},{"text":"branches[]","type":"text","marks":[{"type":"code_inline"}]},{"text":"); tools take the first matching branch, so return a single index (e.g. ","type":"text"},{"text":"[1]","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"7. Wire page processors into branches","type":"text"}]},{"type":"paragraph","content":[{"text":"Each branch contains ","type":"text"},{"text":"pageProcessors[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- an ordered list of exports (lookups) and imports (actions). Each processor has:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"type","type":"text","marks":[{"type":"code_inline"}]},{"text":": ","type":"text"},{"text":"\"export\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"\"import\"","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"_exportId","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"_importId","type":"text","marks":[{"type":"code_inline"}]},{"text":": reference to the resource","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"responseMapping","type":"text","marks":[{"type":"code_inline"}]},{"text":": extract fields from the processor response back into the record -- an object with ","type":"text"},{"text":"fields","type":"text","marks":[{"type":"code_inline"}]},{"text":" (and ","type":"text"},{"text":"lists","type":"text","marks":[{"type":"code_inline"}]},{"text":"), not Mapper 2.0 rows or a transform's ","type":"text"},{"text":"rules","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"hooks.postResponseMap","type":"text","marks":[{"type":"code_inline"}]},{"text":": optional script for post-processing","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"proceedOnFailure","type":"text","marks":[{"type":"code_inline"}]},{"text":": when ","type":"text"},{"text":"true","type":"text","marks":[{"type":"code_inline"}]},{"text":", the later steps and the output stage still run after this step fails (with ","type":"text"},{"text":"false","type":"text","marks":[{"type":"code_inline"}]},{"text":" they're skipped). A direct invoke still answers 422 with the failed step's error either way, so the caller gets no output","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"8. Configure output","type":"text"}]},{"type":"paragraph","content":[{"text":"Output mappings transform the processed data into the tool's return value. Supports:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"mappings[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- Mapper 2.0 rows, such as ","type":"text"},{"text":"{\"generate\": \"orderStatus\", \"extract\": \"$.statusCode\", \"dataType\": \"string\", \"status\": \"Active\"}","type":"text","marks":[{"type":"code_inline"}]},{"text":". A bare ","type":"text"},{"text":"extract","type":"text","marks":[{"type":"code_inline"}]},{"text":" writes that text literally, and a nested target is a parent row with child ","type":"text"},{"text":"mappings","type":"text","marks":[{"type":"code_inline"}]},{"text":", not ","type":"text"},{"text":"a.b","type":"text","marks":[{"type":"code_inline"}]},{"text":" in ","type":"text"},{"text":"generate","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"writing-mappings > Mapper 2.0 Workflow","type":"text","marks":[{"type":"link","attrs":{"href":"../writing-mappings/SKILL.md#mapper-20-workflow","title":null}}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"lookups[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- static key-value enrichment tables","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"hooks.preMap","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"hooks.postMap","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- script hooks before and after mapping","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"schema","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- checked against the mapped output on every run: with ","type":"text"},{"text":"additionalProperties: false","type":"text","marks":[{"type":"code_inline"}]},{"text":", a mapped field the schema doesn't list fails the run with 422 \"Tool output validation failed: must NOT have additional properties\"","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Output mappings and branch response mappings both shape data, but at different times. Branch ","type":"text"},{"text":"responseMapping","type":"text","marks":[{"type":"code_inline"}]},{"text":" merges a processor's response onto the in-flight record so ","type":"text"},{"text":"downstream branches and routers","type":"text","marks":[{"type":"em"}]},{"text":" can use it. Output ","type":"text"},{"text":"mappings","type":"text","marks":[{"type":"code_inline"}]},{"text":" assemble the tool's ","type":"text"},{"text":"return value","type":"text","marks":[{"type":"em"}]},{"text":" at the very end -- and they only see the final in-flight record, not raw processor responses. If a processor's response field must appear in the output, it needs a response mapping on that processor first.","type":"text"}]},{"type":"paragraph","content":[{"text":"A one-lookup tool's router and output. The lookup's first result lands on the record as ","type":"text"},{"text":"item","type":"text","marks":[{"type":"code_inline"}]},{"text":", and each output row reads it with a ","type":"text"},{"text":"$.","type":"text","marks":[{"type":"code_inline"}]},{"text":" path (an output row reading ","type":"text"},{"text":"data[0]","type":"text","marks":[{"type":"code_inline"}]},{"text":" finds nothing, because ","type":"text"},{"text":"data","type":"text","marks":[{"type":"code_inline"}]},{"text":" exists only in the lookup's response):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"\"routers\": [{\n \"id\": \"main\",\n \"branches\": [{\n \"name\": \"Look up the item\",\n \"pageProcessors\": [{\n \"type\": \"export\",\n \"_exportId\": \"\u003clookup export id>\",\n \"responseMapping\": {\"fields\": [{\"extract\": \"data[0]\", \"generate\": \"item\"}], \"lists\": []}\n }],\n \"nextRouterId\": \"outputRouter\"\n }]\n}],\n\"output\": {\n \"mappings\": [\n {\"generate\": \"sku\", \"dataType\": \"string\", \"extract\": \"$.item.sku\", \"status\": \"Active\"},\n {\"generate\": \"onHand\", \"dataType\": \"number\", \"extract\": \"$.item.onHand\", \"status\": \"Active\"}\n ]\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"A one-branch router like this one sets neither ","type":"text"},{"text":"routeRecordsTo","type":"text","marks":[{"type":"code_inline"}]},{"text":" nor ","type":"text"},{"text":"routeRecordsUsing","type":"text","marks":[{"type":"code_inline"}]},{"text":" (Gotcha 4).","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"9. Build the JSON","type":"text"}]},{"type":"paragraph","content":[{"text":"Reference the ","type":"text"},{"text":"Schema Index","type":"text","marks":[{"type":"link","attrs":{"href":"#schema-index","title":null}}]},{"text":" for the exact fields needed. Use the ","type":"text"},{"text":"Which Schemas to Read","type":"text","marks":[{"type":"link","attrs":{"href":"#which-schemas-to-read","title":null}}]},{"text":" decision rule to determine which files to consult.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"10. Test the tool","type":"text"}]},{"type":"paragraph","content":[{"text":"Pass the test input explicitly as ","type":"text"},{"text":"{\"input\": {...}}","type":"text","marks":[{"type":"code_inline"}]},{"text":", in a file passed to ","type":"text"},{"text":"celigo tools test-run \u003cid> --file \u003cpath>","type":"text","marks":[{"type":"code_inline"}]},{"text":". A test run ignores the saved ","type":"text"},{"text":"mockInput","type":"text","marks":[{"type":"code_inline"}]},{"text":" and otherwise starts from an empty record.","type":"text"}]},{"type":"paragraph","content":[{"text":"A lookup export without a saved ","type":"text"},{"text":"mockOutput","type":"text","marks":[{"type":"code_inline"}]},{"text":" calls its live system in a test run, and the run then saves that response on the export as its ","type":"text"},{"text":"mockOutput","type":"text","marks":[{"type":"code_inline"}]},{"text":". Every later test run that uses the export -- from any Tool or API, whatever the input -- answers the lookup from that saved ","type":"text"},{"text":"mockOutput","type":"text","marks":[{"type":"code_inline"}]},{"text":" instead of calling the system; real invokes aren't affected. Read the export's ","type":"text"},{"text":"mockOutput","type":"text","marks":[{"type":"code_inline"}]},{"text":" when test runs return the same lookup result for different inputs, and remove it to test against live data again (","type":"text"},{"text":"celigo exports set \u003cexportId> mockOutput=null","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Imports never send in a test run (each returns its saved ","type":"text"},{"text":"mockResponse","type":"text","marks":[{"type":"code_inline"}]},{"text":" or a placeholder), so a passing run proves the data shaping but not the writes. A test run never appears in the tool's request history. Invoking the tool runs it for real, imports included: ","type":"text"},{"text":"celigo tools invoke \u003cid> --file \u003cpath>","type":"text","marks":[{"type":"code_inline"}]},{"text":" with ","type":"text"},{"text":"{\"input\": {...}, \"overrides\": {\"connections\": [...]}}","type":"text","marks":[{"type":"code_inline"}]},{"text":" (see ","type":"text"},{"text":"Consumer-Bound Connections","type":"text","marks":[{"type":"link","attrs":{"href":"#consumer-bound-connections","title":null}}]},{"text":").","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Observing Runs","type":"text"}]},{"type":"paragraph","content":[{"text":"A tool's runs are recorded only when the caller asks. A direct invoke is recorded with ","type":"text"},{"text":"--log-mode debug","type":"text","marks":[{"type":"code_inline"}]},{"text":" (the ","type":"text"},{"text":"x-log-mode: debug","type":"text","marks":[{"type":"code_inline"}]},{"text":" header); the other modes are accepted and ignored there, and they take effect only when an AI agent, MCP server or guardrail runs the tool. Find a run with ","type":"text"},{"text":"celigo tools request-history \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- the last 5 minutes unless you pass ","type":"text"},{"text":"--time-gte","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"--time-lte","type":"text","marks":[{"type":"code_inline"}]},{"text":", and a run shows up a few seconds after it ends -- then open it with ","type":"text"},{"text":"celigo tools execution-logs \u003cid> \u003cexecutionId>","type":"text","marks":[{"type":"code_inline"}]},{"text":". Recording needs the account's Tool logging entitlement, a sandbox tool answers 403, and test runs never appear. An ","type":"text"},{"text":"input.traceKeyTemplate","type":"text","marks":[{"type":"code_inline"}]},{"text":" (Handlebars with at least one ","type":"text"},{"text":"{{...}}","type":"text","marks":[{"type":"code_inline"}]},{"text":", such as ","type":"text"},{"text":"order-{{record.orderId}}","type":"text","marks":[{"type":"code_inline"}]},{"text":") lets you find runs with ","type":"text"},{"text":"--trace-key-prefix","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"To see what entered or left the tool itself, use its own debug windows (Gotcha 8).","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Refactoring Steps into a Tool","type":"text"}]},{"type":"paragraph","content":[{"text":"A user can ask to factor a contiguous chunk of steps out of an existing flow, API, or tool into a brand-new reusable tool. The parent keeps behaving as before, but the selected steps are replaced by a single tool-step -- a thin wrapper that maps the parent's data into the new tool's input and binds the connections it needs. When the parent is itself a tool, the new resource is a ","type":"text"},{"text":"sub-tool","type":"text","marks":[{"type":"strong"}]},{"text":" and the operation is ","type":"text"},{"text":"tool composition","type":"text","marks":[{"type":"strong"}]},{"text":" -- same mechanics.","type":"text"}]},{"type":"paragraph","content":[{"text":"This operation is ","type":"text"},{"text":"user-driven only.","type":"text","marks":[{"type":"strong"}]},{"text":" Trigger phrases: \"refactor\", \"factor out\", \"extract\", \"turn this into a tool\", \"make this part reusable\", \"pull these steps out as their own tool\". It is never something to propose unprompted -- reuse decisions belong to the user, and the operation creates real Celigo records and can mutate shared resources.","type":"text"}]},{"type":"paragraph","content":[{"text":"How it works:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The named steps are resolved to a contiguous selection with a single entry and a single exit (a valid tool topology).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The new tool's ","type":"text"},{"text":"input","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"output","type":"text","marks":[{"type":"code_inline"}]},{"text":" schemas are designed against the real record shapes flowing through that boundary.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The wrapper is built for the parent to point at, then the parent is rewritten to remove the selected steps and splice in the new tool-step.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Entry and exit nodes never move.","type":"text","marks":[{"type":"strong"}]},{"text":" A flow's trigger steps, an API's request/response bookends, and a tool's input/output nodes are stripped from the selection regardless of whether the user included them -- only the body between them becomes the new tool.","type":"text"}]},{"type":"paragraph","content":[{"text":"No resources are cloned.","type":"text","marks":[{"type":"strong"}]},{"text":" The new tool's page processors reference the SAME export/import IDs the parent used inline. If those underlying exports/imports are then updated to fit the new tool's input/output shape, the change propagates to every other consumer of those resources (other flows, APIs, tools, MCP servers) -- a platform-wide invariant, not a refactor-specific effect.","type":"text"}]},{"type":"paragraph","content":[{"text":"Refactoring cannot be undone.","type":"text","marks":[{"type":"strong"}]},{"text":" There is no programmatic inverse; inlining a tool back into its parent is not supported. If the user regrets a refactor, cleanup is manual: delete the new tool and its wrapper, then restore the parent to its previous configuration. Refactor one parent at a time -- factoring steps from several parents into one shared tool is not a single operation.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"CLI Commands","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# CRUD\nceligo tools list\nceligo tools get \u003cid>\nceligo tools create \u003c tool.json\nceligo tools update \u003cid> \u003c tool.json\nceligo tools set \u003cid> key=value [key2=value2 ...]\nceligo tools dependencies \u003cid> # check before deleting\nceligo tools audit \u003cid> # change history\nceligo tools delete \u003cid>\n\n# Clone into an integration; steps with connections need {\"connectionMap\": {\"\u003cconnId>\": \"\u003cconnId>\"}} (map to itself to reuse)\nceligo tools clone \u003cid> --integration \u003cintegrationId> [--name \u003cname>] [--file \u003cpath>]\n\n# Manage page processors and connections\nceligo tools add-processor \u003cid> \u003cexportOrImportId> [--router \u003crouterId>] [--branch \u003cbranchName>] [-y]\nceligo tools remove-processor \u003cid> \u003cexportOrImportId> [--router \u003crouterId>] [--branch \u003cbranchName>] [-y]\nceligo tools connections \u003cid>\nceligo tools replace-connection \u003cid> \u003cconnectionId> \u003cnewConnectionId> # every step at once, same connection type\n\n# Test run (the --file body is {\"input\": {...}})\nceligo tools test-run \u003cid> --file \u003cpath>\nceligo tools test-run-step-results \u003cid> \u003crunId> \u003cexportOrImportId>\nceligo tools test-run-step-logs \u003cid> \u003crunId> \u003cexportOrImportId>\n\n# Invoke (runs for real; the --file body is {\"input\": {...}, \"overrides\": {\"connections\": [...]}})\nceligo tools invoke \u003cid> --file \u003cpath> [--log-mode debug] # debug records the run in request history\n\n# Run history\nceligo tools request-history \u003cid> [--time-gte \u003ciso>] [--time-lte \u003ciso>]\nceligo tools execution-logs \u003cid> \u003cexecutionId> [--details]\n\n# Debug: a step needs its export's or import's debugUntil; the tool's own stages need input/output.debugUntil\nceligo tools set \u003cid> input.debugUntil=\u003cISO time> # at most 60 minutes ahead; output.debugUntil likewise\nceligo tools debug-requests \u003cid> \u003cexportOrImportId|tool_input|tool_output> [--since \u003cminutes>]\nceligo tools debug-request-detail \u003cid> \u003cexportOrImportId|tool_input|tool_output> \u003ckey>\n\n# Discovery and export\nceligo account search \"\u003ckeyword>\"\nceligo templates marketplace\nceligo tools graph \u003cref> [--as mermaid] # the tool's canvas, from a pulled local tree\nceligo tools download \u003cid> [-o \u003cpath>] # template ZIP","type":"text"}]},{"type":"paragraph","content":[{"text":"\u003c!-- TIER:3 -->","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Pre-Submit Checklist","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Required (all tools)","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":" is set","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"_integrationId","type":"text","marks":[{"type":"code_inline"}]},{"text":" references a valid integration","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"input.schema","type":"text","marks":[{"type":"code_inline"}]},{"text":" is defined with ","type":"text"},{"text":"type: \"object\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" at root (required for MCP compatibility), has no ","type":"text"},{"text":"$schema","type":"text","marks":[{"type":"code_inline"}]},{"text":", and carries ","type":"text"},{"text":"schemaMode: \"json\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" if written as JSON","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"A full update sends ","type":"text"},{"text":"routers","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"input","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"output","type":"text","marks":[{"type":"code_inline"}]},{"text":" whole (with ","type":"text"},{"text":"schemaMode","type":"text","marks":[{"type":"code_inline"}]},{"text":") if it sends them at all","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Pipeline","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"All ","type":"text"},{"text":"_exportId","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"_importId","type":"text","marks":[{"type":"code_inline"}]},{"text":" references in page processors point to existing resources","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Connections for referenced exports/imports are online","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Router IDs are unique within the tool","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"At least one router exists, and every branch has a step and a ","type":"text"},{"text":"nextRouterId","type":"text","marks":[{"type":"code_inline"}]},{"text":" (a router ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"\"outputRouter\"","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No lookup export is a step twice","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Cross-resource consistency","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"If response mapping needed: configured on ","type":"text"},{"text":"pageProcessors[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" entries within branches","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"If routing used: ","type":"text"},{"text":"routeRecordsUsing","type":"text","marks":[{"type":"code_inline"}]},{"text":" and branch filters/scripts are configured correctly","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"If tool is for MCP: tool ","type":"text"},{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":" is unique across all tool and API entries in the MCP server","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"A tool that's already in use gets no new required input (callers that don't send it are refused) and no ","type":"text"},{"text":"additionalProperties: false","type":"text","marks":[{"type":"code_inline"}]},{"text":" on an output its mappings exceed","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Gotchas","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PUT erases omitted fields, except ","type":"text","marks":[{"type":"strong"}]},{"text":"input","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" and ","type":"text","marks":[{"type":"strong"}]},{"text":"output","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":".","type":"text","marks":[{"type":"strong"}]},{"text":" Always GET first, modify, then PUT; the ","type":"text"},{"text":"set","type":"text","marks":[{"type":"code_inline"}]},{"text":" command handles this. A full update without ","type":"text"},{"text":"routers","type":"text","marks":[{"type":"code_inline"}]},{"text":" deletes every step (the next invoke is refused with 400 \"Tool has no steps configured...\"), and one without ","type":"text"},{"text":"description","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"aiDescription","type":"text","marks":[{"type":"code_inline"}]},{"text":" removes them. Left out, ","type":"text"},{"text":"input","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"output","type":"text","marks":[{"type":"code_inline"}]},{"text":" keep their stored fields (only ","type":"text"},{"text":"mockInput","type":"text","marks":[{"type":"code_inline"}]},{"text":" is cleared); sent, each replaces the whole object. ","type":"text"},{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"_integrationId","type":"text","marks":[{"type":"code_inline"}]},{"text":" go on every update. PATCH (","type":"text"},{"text":"celigo tools set","type":"text","marks":[{"type":"code_inline"}]},{"text":" on ","type":"text"},{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"description","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"aiDescription","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"input.debugUntil","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"output.debugUntil","type":"text","marks":[{"type":"code_inline"}]},{"text":") changes one of those without the risk.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Router IDs must be unique within the tool.","type":"text","marks":[{"type":"strong"}]},{"text":" Branch ","type":"text"},{"text":"nextRouterId","type":"text","marks":[{"type":"code_inline"}]},{"text":" must reference a real router ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]},{"text":" or the special ","type":"text"},{"text":"\"outputRouter\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" terminal value.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No dangling branches.","type":"text","marks":[{"type":"strong"}]},{"text":" All branches must merge back to the output node. A branch without ","type":"text"},{"text":"nextRouterId","type":"text","marks":[{"type":"code_inline"}]},{"text":" saves, but every invoke is refused with 422 (and every test run with 400) \"One or more branches are not merged back. Review the setup to ensure all branches converge.\", whichever branch the input would take.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Routing fields come as a pair.","type":"text","marks":[{"type":"strong"}]},{"text":" Set ","type":"text"},{"text":"routeRecordsTo","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"routeRecordsUsing","type":"text","marks":[{"type":"code_inline"}]},{"text":" together on any router that chooses between branches, and leave both off a one-branch pass-through router. Half a pair fails with 422 ","type":"text"},{"text":"invalid_flow_router_config","type":"text","marks":[{"type":"code_inline"}]},{"text":"; several branches with neither field fail with ","type":"text"},{"text":"invalid_flow_router_branch","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Tools cannot be deleted while in use.","type":"text","marks":[{"type":"strong"}]},{"text":" The delete is refused with 422 ","type":"text"},{"text":"dependencies_not_deleted","type":"text","marks":[{"type":"code_inline"}]},{"text":", one error per blocker -- a ","type":"text"},{"text":"ToolImport","type":"text","marks":[{"type":"code_inline"}]},{"text":" step (\"import \u003cid> must be deleted before you can delete tool \u003cid>\"), an MCP server or an access token. List them with ","type":"text"},{"text":"celigo tools dependencies \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":" and remove those references first.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"add-processor","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" auto-creates a router.","type":"text","marks":[{"type":"strong"}]},{"text":" If the tool has no routers, the command creates a default router with one branch. Otherwise it targets the first router's first branch by default -- use ","type":"text"},{"text":"--router","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"--branch","type":"text","marks":[{"type":"code_inline"}]},{"text":" to target a specific location.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Tool names must be unique in the MCP Server.","type":"text","marks":[{"type":"strong"}]},{"text":" The ","type":"text"},{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":" field in ","type":"text"},{"text":"tools[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" on the MCP Server must be unique across all tool AND api entries in that server.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Debug capture is set on each step and on the tool's own Input and Output.","type":"text","marks":[{"type":"strong"}]},{"text":" A step's requests are recorded only while its export or import has ","type":"text"},{"text":"debugUntil","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"celigo exports enable-debug \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"celigo imports enable-debug \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":"); read them with ","type":"text"},{"text":"celigo tools debug-requests \u003cid> \u003cexportOrImportId>","type":"text","marks":[{"type":"code_inline"}]},{"text":". To see each record that entered or left the tool with its schema outcome, failed (422) input validation included, set ","type":"text"},{"text":"celigo tools set \u003cid> input.debugUntil=\u003cISO time>","type":"text","marks":[{"type":"code_inline"}]},{"text":" (or ","type":"text"},{"text":"output.debugUntil","type":"text","marks":[{"type":"code_inline"}]},{"text":") at most 60 minutes ahead -- later is refused with 422 \"...exceeds the maximum allowed difference of 3600 secs.\" -- and read ","type":"text"},{"text":"celigo tools debug-requests \u003cid> tool_input","type":"text","marks":[{"type":"code_inline"}]},{"text":" (or ","type":"text"},{"text":"tool_output","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Capture works for every caller; test runs record nothing.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Test run results may be base64-encoded.","type":"text","marks":[{"type":"strong"}]},{"text":" The CLI auto-decodes these, but raw API responses need manual decoding.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Two different things are called \"lookup.\"","type":"text","marks":[{"type":"strong"}]},{"text":" A branch page-processor lookup (","type":"text"},{"text":"pageProcessors[].type: \"export\"","type":"text","marks":[{"type":"code_inline"}]},{"text":") calls an external system at runtime; an output static lookup table (","type":"text"},{"text":"output.lookups[]","type":"text","marks":[{"type":"code_inline"}]},{"text":") is a fixed value-translation map that reaches nothing. They live in different parts of the tool and are not interchangeable.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Refactoring steps into a tool cannot be undone.","type":"text","marks":[{"type":"strong"}]},{"text":" There is no programmatic inline-back; cleanup is manual (delete the new tool and its wrapper, then restore the parent). The operation is user-driven only -- never initiate it unprompted, and it can update shared exports/imports that other consumers also use.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Keep ","type":"text","marks":[{"type":"strong"}]},{"text":"schemaMode: \"json\"","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" on schemas written as JSON.","type":"text","marks":[{"type":"strong"}]},{"text":" Unset, the UI opens a schema in its visual editor, and an edit there rebuilds it without ","type":"text"},{"text":"format","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"const","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"default","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"additionalProperties","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"$ref","type":"text","marks":[{"type":"code_inline"}]},{"text":" or composition keywords, and turns ","type":"text"},{"text":"integer","type":"text","marks":[{"type":"code_inline"}]},{"text":" into ","type":"text"},{"text":"number","type":"text","marks":[{"type":"code_inline"}]},{"text":". ","type":"text"},{"text":"schemaMode","type":"text","marks":[{"type":"code_inline"}]},{"text":" lives inside ","type":"text"},{"text":"input","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"output","type":"text","marks":[{"type":"code_inline"}]},{"text":", so an update that sends either object must carry it too.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A lookup export is a step only once per tool.","type":"text","marks":[{"type":"strong"}]},{"text":" Using the same export on two branches, or twice in one, fails the save with 422 \"The export \u003cid> is already included as a pageProcessor in your flow, and cannot be added again.\" Put a lookup that every branch needs before the branching router, or give the second branch its own export.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hooks on a tool's steps don't see the integration's settings.","type":"text","marks":[{"type":"strong"}]},{"text":" On a direct invoke, a lookup's ","type":"text"},{"text":"postResponseMap","type":"text","marks":[{"type":"code_inline"}]},{"text":" hook got ","type":"text"},{"text":"options.settings","type":"text","marks":[{"type":"code_inline"}]},{"text":" holding only that export's own ","type":"text"},{"text":"settings.export","type":"text","marks":[{"type":"code_inline"}]},{"text":", though the tool's integration had settings. Pass anything else a hook needs in the input.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A branch filter in the export-filter form is dropped.","type":"text","marks":[{"type":"strong"}]},{"text":" A branch ","type":"text"},{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":" sent as ","type":"text"},{"text":"{\"type\": \"expression\", \"expression\": {...}}","type":"text","marks":[{"type":"code_inline"}]},{"text":" saves without an error and is stored as no filter, so that branch takes every input. Send ","type":"text"},{"text":"{\"version\": \"1\", \"rules\": [...]}","type":"text","marks":[{"type":"code_inline"}]},{"text":" and read the tool back.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Errors","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cause","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fix","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422 \"Expected field: _integrationId to be present.\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Missing integration","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Set ","type":"text"},{"text":"_integrationId","type":"text","marks":[{"type":"code_inline"}]},{"text":" to a valid integration ID, on every create and update","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422 \"Invalid input.schema - no schema with key or ref ...\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The schema's ","type":"text"},{"text":"$schema","type":"text","marks":[{"type":"code_inline"}]},{"text":" names a draft the validator doesn't know (draft 2020-12)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Remove ","type":"text"},{"text":"$schema","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422 \"Duplicate router found with the id ...\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Two routers share an ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Give each router its own ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]},{"text":" and update the ","type":"text"},{"text":"nextRouterId","type":"text","marks":[{"type":"code_inline"}]},{"text":" values that point at it","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422 \"Invalid nextRouterId is defined on the branch. Please correct the branch configuration.\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A ","type":"text"},{"text":"nextRouterId","type":"text","marks":[{"type":"code_inline"}]},{"text":" names no router in the tool","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Point it at a router ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"\"outputRouter\"","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422 on invoke, 400 on a test run: \"One or more branches are not merged back. Review the setup to ensure all branches converge.\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A branch has no ","type":"text"},{"text":"nextRouterId","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Set ","type":"text"},{"text":"nextRouterId","type":"text","marks":[{"type":"code_inline"}]},{"text":" on every branch","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"400 \"Tool has no steps configured. Add at least one step before executing the tool.\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The tool has no router, or a branch has no steps -- also what a full update without ","type":"text"},{"text":"routers","type":"text","marks":[{"type":"code_inline"}]},{"text":" leaves","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Put the steps in a router branch; send ","type":"text"},{"text":"routers","type":"text","marks":[{"type":"code_inline"}]},{"text":" on every full update","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422 \"\u003cid> is not a valid routers.0.branches.0.pageProcessors.0._exportId\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The step names an export (or import) that doesn't exist","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use an id from ","type":"text"},{"text":"celigo exports list","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"celigo imports list","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422 \"The export \u003cid> is already included as a pageProcessor in your flow, and cannot be added again.\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"One export is a step twice, on two branches or in one","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Put the shared lookup before the branching router, or give the second branch its own export","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422 \"Tool input validation failed: must have required property '...'\" (or \"... must match pattern ...\")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The call's input fails ","type":"text"},{"text":"input.schema","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Send what the schema requires, or relax the schema (callers already in use break when a property becomes required)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422 \"Tool output validation failed: must NOT have additional properties\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"output.schema","type":"text","marks":[{"type":"code_inline"}]},{"text":" sets ","type":"text"},{"text":"additionalProperties: false","type":"text","marks":[{"type":"code_inline"}]},{"text":", and the mappings write a field it doesn't list","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"List every mapped field in the schema, or drop ","type":"text"},{"text":"additionalProperties: false","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422 \"import \u003cid> must be deleted before you can delete tool \u003cid>\" (","type":"text"},{"text":"dependencies_not_deleted","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A ","type":"text"},{"text":"ToolImport","type":"text","marks":[{"type":"code_inline"}]},{"text":" step, MCP server or access token still references the tool","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"List them with ","type":"text"},{"text":"celigo tools dependencies \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":", remove those references, then delete","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422 \"...exceeds the maximum allowed difference of 3600 secs.\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"input.debugUntil","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"output.debugUntil","type":"text","marks":[{"type":"code_inline"}]},{"text":" set more than 60 minutes ahead","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Set it at most 60 minutes ahead","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422 ","type":"text"},{"text":"invalid_flow_router_config","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A router sets only one of ","type":"text"},{"text":"routeRecordsTo","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"routeRecordsUsing","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Set both on a router that chooses between branches, or neither on a one-branch pass-through router","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422 ","type":"text"},{"text":"invalid_flow_router_branch","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A router with several branches sets neither routing field","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Set ","type":"text"},{"text":"routeRecordsTo","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"routeRecordsUsing","type":"text","marks":[{"type":"code_inline"}]},{"text":" on it","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"One branch takes every input, and its ","type":"text"},{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":" reads back ","type":"text"},{"text":"null","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The filter was sent as ","type":"text"},{"text":"{\"type\": \"expression\", \"expression\": {...}}","type":"text","marks":[{"type":"code_inline"}]},{"text":", which is stored as no filter","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Send ","type":"text"},{"text":"{\"version\": \"1\", \"rules\": [...]}","type":"text","marks":[{"type":"code_inline"}]},{"text":", then read the tool back","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"connection_not_overridden","type":"text","marks":[{"type":"code_inline"}]},{"text":" (\"Missing connection overrides\")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"An invoke or tool step left a connection the tool's steps use unbound","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Bind every connection, as in ","type":"text"},{"text":"Consumer-Bound Connections","type":"text","marks":[{"type":"link","attrs":{"href":"#consumer-bound-connections","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422 \"Invalid override at overrides.connections[0]. Connection override must include at least one of: _id, ...\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A binding without ","type":"text"},{"text":"_id","type":"text","marks":[{"type":"code_inline"}]},{"text":": the connection named ","type":"text"},{"text":"_connectionId","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"_abstractId","type":"text","marks":[{"type":"code_inline"}]},{"text":" alone","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Send ","type":"text"},{"text":"{\"_abstractId\": \u003cauthored id>, \"_id\": \u003cconnection to run with>}","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"An output field comes back empty or holds its own path as text","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The output row reads the lookup's raw response (","type":"text"},{"text":"data[0]...","type":"text","marks":[{"type":"code_inline"}]},{"text":") or a bare string, or no ","type":"text"},{"text":"responseMapping","type":"text","marks":[{"type":"code_inline"}]},{"text":" carried the field onto the record","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Map the lookup onto the record first (","type":"text"},{"text":"{\"extract\": \"data[0]\", \"generate\": \"item\"}","type":"text","marks":[{"type":"code_inline"}]},{"text":"), then read ","type":"text"},{"text":"$.item.\u003cfield>","type":"text","marks":[{"type":"code_inline"}]},{"text":" in the output row","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A test run answers a lookup with the same record whatever the input","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The lookup export has a saved ","type":"text"},{"text":"mockOutput","type":"text","marks":[{"type":"code_inline"}]},{"text":", which test runs answer from (the first test run that called the lookup saved it)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Read the export's ","type":"text"},{"text":"mockOutput","type":"text","marks":[{"type":"code_inline"}]},{"text":"; remove it to call the live system again","type":"text"}]}]}]}]}]},"metadata":{"date":"2026-11-02","name":"building-tools","author":"@skillopedia","source":{"stars":4,"repo_name":"ai","origin_url":"https://github.com/celigo/ai/blob/HEAD/skills/building-tools/SKILL.md","repo_owner":"celigo","body_sha256":"41915a21ba58dad871fb52a874c66bcd35a62bd04af30c3935b5b530865024fe","cluster_key":"02dc2cde387a3eab2d3d2c6e163734ce3cc7b4bfd41a730b6a031b235457a963","clean_bundle":{"format":"clean-skill-bundle-v1","source":"celigo/ai/skills/building-tools/SKILL.md","text_bytes":122776,"source_commit":"2f3853ed93eece259b8c78ee74ef19035dfae052","skills_sh_rank":6248,"attachment_count":11,"text_attachments":11,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/building-tools/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"integrations-apis","category_label":"Integrations"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"integrations-apis","import_tag":"skills-sh-top20k-2026-11","description":"Build Celigo tool resources -- reusable building blocks that encapsulate lookups, imports, transforms, and branching behind input/output contracts. Callable from flows, APIs, AI agents, MCP servers, and other tools. Use when creating tools, adding steps, or configuring routing."}},"renderedAt":1793666338943}

<!-- TIER:1 -- Building Tools A tool is Celigo's first-class reusable building block . It encapsulates logic -- lookups, imports, transforms, branching -- behind a defined input and output contract. Build it once, use it everywhere: from Flows, APIs, AI Agents, MCP Servers, and other Tools. Tool Concepts Why tools exist: Without tools, users build the same lookup-transform-import patterns repeatedly across Flows, APIs, and Agents. Tools solve this by providing a governed, composable abstraction: one definition, many consumers, consistent behavior. When to build a tool: - You're building an MC…