@withone/cli 1.15.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1714,23 +1714,542 @@ function colorMethod(method) {
1714
1714
  // src/commands/flow.ts
1715
1715
  import pc7 from "picocolors";
1716
1716
 
1717
+ // src/lib/flow-schema.ts
1718
+ var FLOW_SCHEMA = {
1719
+ errorStrategies: ["fail", "continue", "retry", "fallback"],
1720
+ validInputTypes: ["string", "number", "boolean", "object", "array"],
1721
+ flowFields: {
1722
+ key: { type: "string", required: true, description: "Unique kebab-case identifier", pattern: /^[a-z0-9][a-z0-9-]*[a-z0-9]$/ },
1723
+ name: { type: "string", required: true, description: "Human-readable flow name" },
1724
+ description: { type: "string", required: false, description: "What this flow does" },
1725
+ version: { type: "string", required: false, description: "Semver or arbitrary version string" },
1726
+ inputs: { type: "object", required: true, description: "Input declarations (Record<string, InputDeclaration>)" },
1727
+ steps: { type: "array", required: true, description: "Ordered array of steps", stepsArray: true }
1728
+ },
1729
+ inputFields: {
1730
+ type: { type: "string", required: true, description: "Data type: string, number, boolean, object, array", enum: ["string", "number", "boolean", "object", "array"] },
1731
+ required: { type: "boolean", required: false, description: "Whether this input must be provided" },
1732
+ default: { type: "unknown", required: false, description: "Default value if not provided" },
1733
+ description: { type: "string", required: false, description: "Human-readable description" },
1734
+ connection: { type: "object", required: false, description: 'Connection metadata: { platform: "gmail" } \u2014 enables auto-resolution' }
1735
+ },
1736
+ stepCommonFields: {
1737
+ id: { type: "string", required: true, description: "Unique step identifier (used in selectors)" },
1738
+ name: { type: "string", required: true, description: "Human-readable step label" },
1739
+ type: { type: "string", required: true, description: "Step type (determines which config object is required)" },
1740
+ if: { type: "string", required: false, description: "JS expression \u2014 skip step if falsy" },
1741
+ unless: { type: "string", required: false, description: "JS expression \u2014 skip step if truthy" }
1742
+ },
1743
+ stepTypes: [
1744
+ {
1745
+ type: "action",
1746
+ configKey: "action",
1747
+ description: "Execute a platform API action",
1748
+ fields: {
1749
+ platform: { type: "string", required: true, description: "Platform name (kebab-case)" },
1750
+ actionId: { type: "string", required: true, description: "Action ID from `actions search`" },
1751
+ connectionKey: { type: "string", required: true, description: "Connection key (use $.input selector)" },
1752
+ data: { type: "object", required: false, description: "Request body (POST/PUT/PATCH)" },
1753
+ pathVars: { type: "object", required: false, description: "URL path variables" },
1754
+ queryParams: { type: "object", required: false, description: "Query parameters" },
1755
+ headers: { type: "object", required: false, description: "Additional headers" }
1756
+ },
1757
+ example: {
1758
+ id: "findCustomer",
1759
+ name: "Search Stripe customers",
1760
+ type: "action",
1761
+ action: {
1762
+ platform: "stripe",
1763
+ actionId: "conn_mod_def::xxx::yyy",
1764
+ connectionKey: "$.input.stripeConnectionKey",
1765
+ data: { query: "email:'{{$.input.customerEmail}}'" }
1766
+ }
1767
+ }
1768
+ },
1769
+ {
1770
+ type: "transform",
1771
+ configKey: "transform",
1772
+ description: "Single JS expression with implicit return",
1773
+ fields: {
1774
+ expression: { type: "string", required: true, description: "JS expression evaluated with flow context as $" }
1775
+ },
1776
+ example: {
1777
+ id: "extractNames",
1778
+ name: "Extract customer names",
1779
+ type: "transform",
1780
+ transform: { expression: "$.steps.findCustomer.response.data.map(c => c.name)" }
1781
+ }
1782
+ },
1783
+ {
1784
+ type: "code",
1785
+ configKey: "code",
1786
+ description: "Multi-line async JS with explicit return",
1787
+ fields: {
1788
+ source: { type: "string", required: true, description: "JS function body (flow context as $, supports await)" }
1789
+ },
1790
+ example: {
1791
+ id: "processData",
1792
+ name: "Process and enrich data",
1793
+ type: "code",
1794
+ code: { source: "const items = $.steps.fetch.response.data;\nreturn items.filter(i => i.active);" }
1795
+ }
1796
+ },
1797
+ {
1798
+ type: "condition",
1799
+ configKey: "condition",
1800
+ description: "If/then/else branching",
1801
+ fields: {
1802
+ expression: { type: "string", required: true, description: "JS expression \u2014 truthy runs then, falsy runs else" },
1803
+ then: { type: "array", required: true, description: "Steps to run when true", stepsArray: true },
1804
+ else: { type: "array", required: false, description: "Steps to run when false", stepsArray: true }
1805
+ },
1806
+ example: {
1807
+ id: "checkFound",
1808
+ name: "Check if customer exists",
1809
+ type: "condition",
1810
+ condition: {
1811
+ expression: "$.steps.search.response.data.length > 0",
1812
+ then: [{ id: "notify", name: "Send notification", type: "action", action: { platform: "slack", actionId: "...", connectionKey: "$.input.slackKey", data: { text: "Found!" } } }],
1813
+ else: [{ id: "logMiss", name: "Log not found", type: "transform", transform: { expression: "'Not found'" } }]
1814
+ }
1815
+ }
1816
+ },
1817
+ {
1818
+ type: "loop",
1819
+ configKey: "loop",
1820
+ description: "Iterate over an array with optional concurrency",
1821
+ fields: {
1822
+ over: { type: "string", required: true, description: "Selector resolving to an array" },
1823
+ as: { type: "string", required: true, description: "Variable name for current item ($.loop.<as>)" },
1824
+ indexAs: { type: "string", required: false, description: "Variable name for index" },
1825
+ steps: { type: "array", required: true, description: "Steps to run per iteration", stepsArray: true },
1826
+ maxIterations: { type: "number", required: false, description: "Safety cap (default: no limit)" },
1827
+ maxConcurrency: { type: "number", required: false, description: "Parallel batch size (default: 1 = sequential)" }
1828
+ },
1829
+ example: {
1830
+ id: "processOrders",
1831
+ name: "Process each order",
1832
+ type: "loop",
1833
+ loop: {
1834
+ over: "$.steps.listOrders.response.data",
1835
+ as: "order",
1836
+ steps: [{ id: "createInvoice", name: "Create invoice", type: "action", action: { platform: "stripe", actionId: "...", connectionKey: "$.input.stripeKey", data: { amount: "$.loop.order.total" } } }]
1837
+ }
1838
+ }
1839
+ },
1840
+ {
1841
+ type: "parallel",
1842
+ configKey: "parallel",
1843
+ description: "Run steps concurrently",
1844
+ fields: {
1845
+ steps: { type: "array", required: true, description: "Steps to run in parallel", stepsArray: true },
1846
+ maxConcurrency: { type: "number", required: false, description: "Max concurrent steps (default: 5)" }
1847
+ },
1848
+ example: {
1849
+ id: "lookups",
1850
+ name: "Parallel data lookups",
1851
+ type: "parallel",
1852
+ parallel: {
1853
+ steps: [
1854
+ { id: "getStripe", name: "Get Stripe data", type: "action", action: { platform: "stripe", actionId: "...", connectionKey: "$.input.stripeKey" } },
1855
+ { id: "getSlack", name: "Get Slack data", type: "action", action: { platform: "slack", actionId: "...", connectionKey: "$.input.slackKey" } }
1856
+ ]
1857
+ }
1858
+ }
1859
+ },
1860
+ {
1861
+ type: "file-read",
1862
+ configKey: "fileRead",
1863
+ description: "Read a file (optional JSON parse)",
1864
+ fields: {
1865
+ path: { type: "string", required: true, description: "File path to read" },
1866
+ parseJson: { type: "boolean", required: false, description: "Parse contents as JSON (default: false)" }
1867
+ },
1868
+ example: {
1869
+ id: "readConfig",
1870
+ name: "Read config file",
1871
+ type: "file-read",
1872
+ fileRead: { path: "./data/config.json", parseJson: true }
1873
+ }
1874
+ },
1875
+ {
1876
+ type: "file-write",
1877
+ configKey: "fileWrite",
1878
+ description: "Write or append to a file",
1879
+ fields: {
1880
+ path: { type: "string", required: true, description: "File path to write" },
1881
+ content: { type: "unknown", required: true, description: "Content to write (supports selectors)" },
1882
+ append: { type: "boolean", required: false, description: "Append instead of overwrite (default: false)" }
1883
+ },
1884
+ example: {
1885
+ id: "writeResults",
1886
+ name: "Save results",
1887
+ type: "file-write",
1888
+ fileWrite: { path: "./output/results.json", content: "$.steps.transform.output" }
1889
+ }
1890
+ },
1891
+ {
1892
+ type: "while",
1893
+ configKey: "while",
1894
+ description: "Do-while loop with condition check",
1895
+ fields: {
1896
+ condition: { type: "string", required: true, description: "JS expression checked before each iteration (after first)" },
1897
+ steps: { type: "array", required: true, description: "Steps to run each iteration", stepsArray: true },
1898
+ maxIterations: { type: "number", required: false, description: "Safety cap (default: 100)" }
1899
+ },
1900
+ example: {
1901
+ id: "paginate",
1902
+ name: "Paginate through pages",
1903
+ type: "while",
1904
+ while: {
1905
+ condition: "$.steps.paginate.output.lastResult.nextPageToken != null",
1906
+ maxIterations: 50,
1907
+ steps: [{ id: "fetchPage", name: "Fetch next page", type: "action", action: { platform: "gmail", actionId: "...", connectionKey: "$.input.gmailKey" } }]
1908
+ }
1909
+ }
1910
+ },
1911
+ {
1912
+ type: "flow",
1913
+ configKey: "flow",
1914
+ description: "Execute a sub-flow (supports composition)",
1915
+ fields: {
1916
+ key: { type: "string", required: true, description: "Flow key or path of the sub-flow" },
1917
+ inputs: { type: "object", required: false, description: "Inputs to pass to the sub-flow (supports selectors)" }
1918
+ },
1919
+ example: {
1920
+ id: "enrich",
1921
+ name: "Run enrichment sub-flow",
1922
+ type: "flow",
1923
+ flow: { key: "enrich-customer", inputs: { email: "$.steps.getCustomer.response.email" } }
1924
+ }
1925
+ },
1926
+ {
1927
+ type: "paginate",
1928
+ configKey: "paginate",
1929
+ description: "Auto-paginate API results into a single array",
1930
+ fields: {
1931
+ action: { type: "object", required: true, description: "Action config (same shape as action step: platform, actionId, connectionKey)" },
1932
+ pageTokenField: { type: "string", required: true, description: "Dot-path in response to next page token" },
1933
+ resultsField: { type: "string", required: true, description: "Dot-path in response to results array" },
1934
+ inputTokenParam: { type: "string", required: true, description: "Dot-path in action config where page token is injected" },
1935
+ maxPages: { type: "number", required: false, description: "Max pages to fetch (default: 10)" }
1936
+ },
1937
+ example: {
1938
+ id: "allMessages",
1939
+ name: "Fetch all Gmail messages",
1940
+ type: "paginate",
1941
+ paginate: {
1942
+ action: { platform: "gmail", actionId: "...", connectionKey: "$.input.gmailKey", queryParams: { maxResults: 100 } },
1943
+ pageTokenField: "nextPageToken",
1944
+ resultsField: "messages",
1945
+ inputTokenParam: "queryParams.pageToken",
1946
+ maxPages: 10
1947
+ }
1948
+ }
1949
+ },
1950
+ {
1951
+ type: "bash",
1952
+ configKey: "bash",
1953
+ description: "Shell command (requires --allow-bash)",
1954
+ fields: {
1955
+ command: { type: "string", required: true, description: "Shell command to execute (supports selectors)" },
1956
+ timeout: { type: "number", required: false, description: "Timeout in ms (default: 30000)" },
1957
+ parseJson: { type: "boolean", required: false, description: "Parse stdout as JSON (default: false)" },
1958
+ cwd: { type: "string", required: false, description: "Working directory (supports selectors)" },
1959
+ env: { type: "object", required: false, description: "Additional environment variables" }
1960
+ },
1961
+ example: {
1962
+ id: "analyze",
1963
+ name: "Analyze with Claude",
1964
+ type: "bash",
1965
+ bash: {
1966
+ command: "cat /tmp/data.json | claude --print 'Analyze this data' --output-format json",
1967
+ timeout: 18e4,
1968
+ parseJson: true
1969
+ }
1970
+ }
1971
+ }
1972
+ ]
1973
+ };
1974
+ var _coveredTypes = Object.fromEntries(
1975
+ FLOW_SCHEMA.stepTypes.map((st) => [st.type, true])
1976
+ );
1977
+ var _stepTypeMap = new Map(
1978
+ FLOW_SCHEMA.stepTypes.map((st) => [st.type, st])
1979
+ );
1980
+ function getStepTypeDescriptor(type) {
1981
+ return _stepTypeMap.get(type);
1982
+ }
1983
+ function getValidStepTypes() {
1984
+ return FLOW_SCHEMA.stepTypes.map((st) => st.type);
1985
+ }
1986
+ function getNestedStepsKeys() {
1987
+ const result = [];
1988
+ for (const st of FLOW_SCHEMA.stepTypes) {
1989
+ for (const [fieldName, fd] of Object.entries(st.fields)) {
1990
+ if (fd.stepsArray) {
1991
+ result.push({ configKey: st.configKey, fieldName });
1992
+ }
1993
+ }
1994
+ }
1995
+ return result;
1996
+ }
1997
+ function generateFlowGuide() {
1998
+ const validTypes = getValidStepTypes();
1999
+ const sections = [];
2000
+ sections.push(`# One Flows \u2014 Reference
2001
+
2002
+ ## Overview
2003
+
2004
+ Workflows are JSON files at \`.one/flows/<key>.flow.json\` that chain actions across platforms.
2005
+
2006
+ ## Commands
2007
+
2008
+ \`\`\`bash
2009
+ one --agent flow create <key> --definition '<json>' # Create (or --definition @file.json)
2010
+ one --agent flow create <key> --definition @flow.json # Create from file
2011
+ one --agent flow list # List
2012
+ one --agent flow validate <key> # Validate
2013
+ one --agent flow execute <key> -i name=value # Execute
2014
+ one --agent flow execute <key> --dry-run --mock # Test with mock data
2015
+ one --agent flow execute <key> --allow-bash # Enable bash steps
2016
+ one --agent flow runs [flowKey] # List past runs
2017
+ one --agent flow resume <runId> # Resume failed run
2018
+ one --agent flow scaffold [template] # Generate a starter template
2019
+ \`\`\`
2020
+
2021
+ You can also write the JSON file directly to \`.one/flows/<key>.flow.json\` \u2014 this is often easier than passing large JSON via --definition.
2022
+
2023
+ ## Building a Workflow
2024
+
2025
+ 1. **Design first** \u2014 clarify the end goal, map the full value chain, identify where AI analysis is needed
2026
+ 2. **Discover connections** \u2014 \`one --agent connection list\`
2027
+ 3. **Get knowledge** for every action \u2014 \`one --agent actions knowledge <platform> <actionId>\`
2028
+ 4. **Construct JSON** \u2014 declare inputs, wire steps with selectors
2029
+ 5. **Validate** \u2014 \`one --agent flow validate <key>\`
2030
+ 6. **Execute** \u2014 \`one --agent flow execute <key> -i param=value\``);
2031
+ sections.push(`## Flow JSON Schema
2032
+
2033
+ \`\`\`json
2034
+ {
2035
+ "key": "my-workflow",
2036
+ "name": "My Workflow",
2037
+ "description": "What this flow does",
2038
+ "version": "1",
2039
+ "inputs": {
2040
+ "connectionKey": {
2041
+ "type": "string",
2042
+ "required": true,
2043
+ "description": "Platform connection key",
2044
+ "connection": { "platform": "stripe" }
2045
+ },
2046
+ "param": {
2047
+ "type": "string",
2048
+ "required": true,
2049
+ "description": "A user parameter"
2050
+ }
2051
+ },
2052
+ "steps": [
2053
+ {
2054
+ "id": "stepId",
2055
+ "name": "Human-readable step name",
2056
+ "type": "action",
2057
+ "action": {
2058
+ "platform": "stripe",
2059
+ "actionId": "conn_mod_def::xxx::yyy",
2060
+ "connectionKey": "$.input.connectionKey",
2061
+ "data": { "query": "{{$.input.param}}" }
2062
+ }
2063
+ }
2064
+ ]
2065
+ }
2066
+ \`\`\`
2067
+
2068
+ ### Top-level fields
2069
+
2070
+ | Field | Type | Required | Description |
2071
+ |-------|------|----------|-------------|`);
2072
+ for (const [name, fd] of Object.entries(FLOW_SCHEMA.flowFields)) {
2073
+ sections.push(`| \`${name}\` | ${fd.type} | ${fd.required ? "yes" : "no"} | ${fd.description} |`);
2074
+ }
2075
+ sections.push(`
2076
+ ### Input declarations
2077
+
2078
+ | Field | Type | Required | Description |
2079
+ |-------|------|----------|-------------|`);
2080
+ for (const [name, fd] of Object.entries(FLOW_SCHEMA.inputFields)) {
2081
+ sections.push(`| \`${name}\` | ${fd.type} | ${fd.required ? "yes" : "no"} | ${fd.description} |`);
2082
+ }
2083
+ sections.push(`
2084
+ ### Step fields (all steps)
2085
+
2086
+ Every step MUST have \`id\`, \`name\`, and \`type\`. The \`type\` determines which config object is required.
2087
+
2088
+ | Field | Type | Required | Description |
2089
+ |-------|------|----------|-------------|`);
2090
+ for (const [name, fd] of Object.entries(FLOW_SCHEMA.stepCommonFields)) {
2091
+ sections.push(`| \`${name}\` | ${fd.type} | ${fd.required ? "yes" : "no"} | ${fd.description} |`);
2092
+ }
2093
+ sections.push(`| \`onError\` | object | no | Error handling: \`{ "strategy": "${FLOW_SCHEMA.errorStrategies.join(" | ")}", "retries": 3, "retryDelayMs": 1000 }\` |`);
2094
+ sections.push(`
2095
+ ## Step Types
2096
+
2097
+ **IMPORTANT:** Each step type requires a config object nested under a specific key. The type name and config key differ for some types (noted below).
2098
+
2099
+ | Type | Config Key | Description |
2100
+ |------|-----------|-------------|`);
2101
+ for (const st of FLOW_SCHEMA.stepTypes) {
2102
+ const keyNote = st.type !== st.configKey ? ` \u26A0\uFE0F` : "";
2103
+ sections.push(`| \`${st.type}\` | \`${st.configKey}\`${keyNote} | ${st.description} |`);
2104
+ }
2105
+ sections.push(`
2106
+ ## Step Type Reference`);
2107
+ for (const st of FLOW_SCHEMA.stepTypes) {
2108
+ sections.push(`
2109
+ ### \`${st.type}\` \u2014 ${st.description}`);
2110
+ if (st.type !== st.configKey) {
2111
+ sections.push(`
2112
+ > **Note:** Type is \`"${st.type}"\` but config key is \`"${st.configKey}"\` (camelCase).`);
2113
+ }
2114
+ sections.push(`
2115
+ | Field | Type | Required | Description |
2116
+ |-------|------|----------|-------------|`);
2117
+ for (const [name, fd] of Object.entries(st.fields)) {
2118
+ sections.push(`| \`${name}\` | ${fd.type} | ${fd.required ? "yes" : "no"} | ${fd.description} |`);
2119
+ }
2120
+ sections.push(`
2121
+ \`\`\`json
2122
+ ${JSON.stringify(st.example, null, 2)}
2123
+ \`\`\``);
2124
+ }
2125
+ sections.push(`
2126
+ ## Selectors
2127
+
2128
+ | Pattern | Resolves To |
2129
+ |---------|-------------|
2130
+ | \`$.input.paramName\` | Input value |
2131
+ | \`$.steps.stepId.response\` | Full API response |
2132
+ | \`$.steps.stepId.response.data[0].email\` | Nested field |
2133
+ | \`$.steps.stepId.response.data[*].id\` | Wildcard array map |
2134
+ | \`$.env.MY_VAR\` | Environment variable |
2135
+ | \`$.loop.item\` / \`$.loop.i\` | Loop iteration |
2136
+ | \`"Hello {{$.steps.getUser.response.name}}"\` | String interpolation |
2137
+
2138
+ ### When to use bare selectors vs \`{{...}}\` interpolation
2139
+
2140
+ - **Bare selectors** (\`$.input.x\`): Use for fields the engine resolves directly \u2014 \`connectionKey\`, \`over\`, \`path\`, \`expression\`, \`condition\`, and any field where the entire value is a single selector. The resolved value keeps its original type (object, array, number).
2141
+ - **Interpolation** (\`{{$.input.x}}\`): Use inside string values where the selector is embedded in text \u2014 e.g., \`"Hello {{$.steps.getUser.response.name}}"\`. The resolved value is always stringified. Use this in \`data\`, \`pathVars\`, and \`queryParams\` when mixing selectors with literal text.
2142
+ - **Rule of thumb**: If the value is purely a selector, use bare. If it's a string containing a selector, use \`{{...}}\`.
2143
+
2144
+ ### \`output\` vs \`response\` on step results
2145
+
2146
+ Every completed step produces both \`output\` and \`response\`:
2147
+ - **Action steps**: \`response\` is the raw API response. \`output\` is the same as \`response\`.
2148
+ - **Code/transform steps**: \`output\` is the return value. \`response\` is an alias for \`output\`.
2149
+ - **In practice**: Use \`$.steps.stepId.response\` for action steps (API data) and \`$.steps.stepId.output\` for code/transform steps (computed data). Both work interchangeably, but using the semantically correct one makes flows easier to read.
2150
+
2151
+ ## Error Handling
2152
+
2153
+ \`\`\`json
2154
+ {"onError": {"strategy": "retry", "retries": 3, "retryDelayMs": 1000}}
2155
+ \`\`\`
2156
+
2157
+ Strategies: \`${FLOW_SCHEMA.errorStrategies.join("`, `")}\`
2158
+
2159
+ Conditional execution: \`"if": "$.steps.prev.response.data.length > 0"\`
2160
+
2161
+ ## Input Connection Auto-Resolution
2162
+
2163
+ When an input has \`"connection": { "platform": "stripe" }\`, the flow engine can automatically resolve the connection key at execution time. If the user has exactly one connection for that platform, the engine fills in the key without requiring \`-i connectionKey=...\`. If multiple connections exist, the user must specify which one. This is metadata for tooling \u2014 it does not affect the flow JSON structure, but it makes execution more convenient.
2164
+
2165
+ ## Complete Example: Fetch Data, Transform, Notify
2166
+
2167
+ \`\`\`json
2168
+ {
2169
+ "key": "contacts-to-slack",
2170
+ "name": "CRM Contacts Summary to Slack",
2171
+ "description": "Fetch recent contacts from CRM, build a summary, post to Slack",
2172
+ "version": "1",
2173
+ "inputs": {
2174
+ "crmConnectionKey": {
2175
+ "type": "string",
2176
+ "required": true,
2177
+ "description": "CRM platform connection key",
2178
+ "connection": { "platform": "attio" }
2179
+ },
2180
+ "slackConnectionKey": {
2181
+ "type": "string",
2182
+ "required": true,
2183
+ "description": "Slack connection key",
2184
+ "connection": { "platform": "slack" }
2185
+ },
2186
+ "slackChannel": {
2187
+ "type": "string",
2188
+ "required": true,
2189
+ "description": "Slack channel name or ID"
2190
+ }
2191
+ },
2192
+ "steps": [
2193
+ {
2194
+ "id": "fetchContacts",
2195
+ "name": "Fetch recent contacts",
2196
+ "type": "action",
2197
+ "action": {
2198
+ "platform": "attio",
2199
+ "actionId": "ATTIO_LIST_PEOPLE_ACTION_ID",
2200
+ "connectionKey": "$.input.crmConnectionKey",
2201
+ "queryParams": { "limit": "10" }
2202
+ }
2203
+ },
2204
+ {
2205
+ "id": "buildSummary",
2206
+ "name": "Build formatted summary",
2207
+ "type": "code",
2208
+ "code": {
2209
+ "source": "const contacts = $.steps.fetchContacts.response.data || [];\\nconst lines = contacts.map((c, i) => \`\${i+1}. \${c.name || 'Unknown'} \u2014 \${c.email || 'no email'}\`);\\nreturn { summary: \`Found \${contacts.length} contacts:\\n\${lines.join('\\n')}\` };"
2210
+ }
2211
+ },
2212
+ {
2213
+ "id": "notifySlack",
2214
+ "name": "Post summary to Slack",
2215
+ "type": "action",
2216
+ "action": {
2217
+ "platform": "slack",
2218
+ "actionId": "SLACK_SEND_MESSAGE_ACTION_ID",
2219
+ "connectionKey": "$.input.slackConnectionKey",
2220
+ "data": {
2221
+ "channel": "$.input.slackChannel",
2222
+ "text": "{{$.steps.buildSummary.output.summary}}"
2223
+ }
2224
+ }
2225
+ }
2226
+ ]
2227
+ }
2228
+ \`\`\`
2229
+
2230
+ Note: Action IDs above are placeholders. Always use \`one --agent actions search <platform> "<query>"\` to find real IDs.
2231
+
2232
+ ## AI-Augmented Pattern
2233
+
2234
+ For workflows that need analysis/summarization, use the file-write \u2192 bash \u2192 code pattern:
2235
+
2236
+ 1. \`file-write\` \u2014 save data to temp file
2237
+ 2. \`bash\` \u2014 \`claude --print\` analyzes it (\`parseJson: true\`, \`timeout: 180000\`)
2238
+ 3. \`code\` \u2014 parse and structure the output
2239
+
2240
+ Set timeout to at least 180000ms (3 min). Run Claude-heavy flows sequentially, not in parallel.
2241
+
2242
+ ## Notes
2243
+
2244
+ - Connection keys are **inputs**, not hardcoded
2245
+ - Action IDs in examples are placeholders \u2014 always use \`actions search\`
2246
+ - Code steps allow \`crypto\`, \`buffer\`, \`url\`, \`path\` \u2014 \`fs\`, \`http\`, \`child_process\` are blocked
2247
+ - Bash steps require \`--allow-bash\` flag
2248
+ - State is persisted after every step \u2014 resume picks up where it left off`);
2249
+ return sections.join("\n");
2250
+ }
2251
+
1717
2252
  // src/lib/flow-validator.ts
1718
- var VALID_STEP_TYPES = [
1719
- "action",
1720
- "transform",
1721
- "code",
1722
- "condition",
1723
- "loop",
1724
- "parallel",
1725
- "file-read",
1726
- "file-write",
1727
- "while",
1728
- "flow",
1729
- "paginate",
1730
- "bash"
1731
- ];
1732
- var VALID_INPUT_TYPES = ["string", "number", "boolean", "object", "array"];
1733
- var VALID_ERROR_STRATEGIES = ["fail", "continue", "retry", "fallback"];
1734
2253
  function validateFlowSchema(flow2) {
1735
2254
  const errors = [];
1736
2255
  if (!flow2 || typeof flow2 !== "object") {
@@ -1740,7 +2259,7 @@ function validateFlowSchema(flow2) {
1740
2259
  const f = flow2;
1741
2260
  if (!f.key || typeof f.key !== "string") {
1742
2261
  errors.push({ path: "key", message: 'Flow must have a string "key"' });
1743
- } else if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(f.key) && f.key.length > 1) {
2262
+ } else if (FLOW_SCHEMA.flowFields.key.pattern && !FLOW_SCHEMA.flowFields.key.pattern.test(f.key) && f.key.length > 1) {
1744
2263
  errors.push({ path: "key", message: "Flow key must be kebab-case (lowercase letters, numbers, hyphens)" });
1745
2264
  }
1746
2265
  if (!f.name || typeof f.name !== "string") {
@@ -1763,8 +2282,8 @@ function validateFlowSchema(flow2) {
1763
2282
  continue;
1764
2283
  }
1765
2284
  const d = decl;
1766
- if (!d.type || !VALID_INPUT_TYPES.includes(d.type)) {
1767
- errors.push({ path: `${prefix}.type`, message: `Input type must be one of: ${VALID_INPUT_TYPES.join(", ")}` });
2285
+ if (!d.type || !FLOW_SCHEMA.validInputTypes.includes(d.type)) {
2286
+ errors.push({ path: `${prefix}.type`, message: `Input type must be one of: ${FLOW_SCHEMA.validInputTypes.join(", ")}` });
1768
2287
  }
1769
2288
  if (d.connection !== void 0) {
1770
2289
  if (!d.connection || typeof d.connection !== "object") {
@@ -1786,6 +2305,7 @@ function validateFlowSchema(flow2) {
1786
2305
  return errors;
1787
2306
  }
1788
2307
  function validateStepsArray(steps, pathPrefix, errors) {
2308
+ const validTypes = FLOW_SCHEMA.stepTypes.map((st) => st.type);
1789
2309
  for (let i = 0; i < steps.length; i++) {
1790
2310
  const step = steps[i];
1791
2311
  const path4 = `${pathPrefix}[${i}]`;
@@ -1800,189 +2320,79 @@ function validateStepsArray(steps, pathPrefix, errors) {
1800
2320
  if (!s.name || typeof s.name !== "string") {
1801
2321
  errors.push({ path: `${path4}.name`, message: 'Step must have a string "name"' });
1802
2322
  }
1803
- if (!s.type || !VALID_STEP_TYPES.includes(s.type)) {
1804
- errors.push({ path: `${path4}.type`, message: `Step type must be one of: ${VALID_STEP_TYPES.join(", ")}` });
2323
+ if (!s.type || !validTypes.includes(s.type)) {
2324
+ errors.push({ path: `${path4}.type`, message: `Step type must be one of: ${validTypes.join(", ")}` });
2325
+ continue;
1805
2326
  }
1806
2327
  if (s.onError && typeof s.onError === "object") {
1807
2328
  const oe = s.onError;
1808
- if (!VALID_ERROR_STRATEGIES.includes(oe.strategy)) {
1809
- errors.push({ path: `${path4}.onError.strategy`, message: `Error strategy must be one of: ${VALID_ERROR_STRATEGIES.join(", ")}` });
2329
+ if (!FLOW_SCHEMA.errorStrategies.includes(oe.strategy)) {
2330
+ errors.push({ path: `${path4}.onError.strategy`, message: `Error strategy must be one of: ${FLOW_SCHEMA.errorStrategies.join(", ")}` });
1810
2331
  }
1811
2332
  }
1812
- const type = s.type;
1813
- if (type === "action") {
1814
- if (!s.action || typeof s.action !== "object") {
1815
- errors.push({ path: `${path4}.action`, message: 'Action step must have an "action" config object' });
1816
- } else {
1817
- const a = s.action;
1818
- if (!a.platform) errors.push({ path: `${path4}.action.platform`, message: 'Action must have "platform"' });
1819
- if (!a.actionId) errors.push({ path: `${path4}.action.actionId`, message: 'Action must have "actionId"' });
1820
- if (!a.connectionKey) errors.push({ path: `${path4}.action.connectionKey`, message: 'Action must have "connectionKey"' });
1821
- }
1822
- } else if (type === "transform") {
1823
- if (!s.transform || typeof s.transform !== "object") {
1824
- errors.push({ path: `${path4}.transform`, message: 'Transform step must have a "transform" config object' });
1825
- } else {
1826
- const t = s.transform;
1827
- if (!t.expression || typeof t.expression !== "string") {
1828
- errors.push({ path: `${path4}.transform.expression`, message: 'Transform must have a string "expression"' });
1829
- }
1830
- }
1831
- } else if (type === "code") {
1832
- if (!s.code || typeof s.code !== "object") {
1833
- errors.push({ path: `${path4}.code`, message: 'Code step must have a "code" config object' });
1834
- } else {
1835
- const c = s.code;
1836
- if (!c.source || typeof c.source !== "string") {
1837
- errors.push({ path: `${path4}.code.source`, message: 'Code must have a string "source"' });
1838
- }
1839
- }
1840
- } else if (type === "condition") {
1841
- if (!s.condition || typeof s.condition !== "object") {
1842
- errors.push({ path: `${path4}.condition`, message: 'Condition step must have a "condition" config object' });
1843
- } else {
1844
- const c = s.condition;
1845
- if (!c.expression || typeof c.expression !== "string") {
1846
- errors.push({ path: `${path4}.condition.expression`, message: 'Condition must have a string "expression"' });
1847
- }
1848
- if (!Array.isArray(c.then)) {
1849
- errors.push({ path: `${path4}.condition.then`, message: 'Condition must have a "then" steps array' });
1850
- } else {
1851
- validateStepsArray(c.then, `${path4}.condition.then`, errors);
1852
- }
1853
- if (c.else !== void 0) {
1854
- if (!Array.isArray(c.else)) {
1855
- errors.push({ path: `${path4}.condition.else`, message: 'Condition "else" must be a steps array' });
1856
- } else {
1857
- validateStepsArray(c.else, `${path4}.condition.else`, errors);
1858
- }
1859
- }
1860
- }
1861
- } else if (type === "loop") {
1862
- if (!s.loop || typeof s.loop !== "object") {
1863
- errors.push({ path: `${path4}.loop`, message: 'Loop step must have a "loop" config object' });
1864
- } else {
1865
- const l = s.loop;
1866
- if (!l.over || typeof l.over !== "string") {
1867
- errors.push({ path: `${path4}.loop.over`, message: 'Loop must have a string "over" selector' });
1868
- }
1869
- if (!l.as || typeof l.as !== "string") {
1870
- errors.push({ path: `${path4}.loop.as`, message: 'Loop must have a string "as" variable name' });
1871
- }
1872
- if (!Array.isArray(l.steps)) {
1873
- errors.push({ path: `${path4}.loop.steps`, message: 'Loop must have a "steps" array' });
1874
- } else {
1875
- validateStepsArray(l.steps, `${path4}.loop.steps`, errors);
1876
- }
2333
+ const descriptor = getStepTypeDescriptor(s.type);
2334
+ if (!descriptor) continue;
2335
+ const configKey = descriptor.configKey;
2336
+ const configObj = s[configKey];
2337
+ if (!configObj || typeof configObj !== "object") {
2338
+ const hint = detectFlatConfigHint(s, descriptor);
2339
+ errors.push({
2340
+ path: `${path4}.${configKey}`,
2341
+ message: `${capitalize(descriptor.type)} step must have a "${configKey}" config object${hint}`
2342
+ });
2343
+ continue;
2344
+ }
2345
+ const config = configObj;
2346
+ for (const [fieldName, fd] of Object.entries(descriptor.fields)) {
2347
+ const fieldPath = `${path4}.${configKey}.${fieldName}`;
2348
+ const value = config[fieldName];
2349
+ if (fd.required && (value === void 0 || value === null || value === "")) {
2350
+ errors.push({ path: fieldPath, message: `${capitalize(descriptor.type)} must have ${fd.type === "string" ? "a string" : fd.type === "array" ? "a" : "a"} "${fieldName}"` });
2351
+ continue;
1877
2352
  }
1878
- } else if (type === "parallel") {
1879
- if (!s.parallel || typeof s.parallel !== "object") {
1880
- errors.push({ path: `${path4}.parallel`, message: 'Parallel step must have a "parallel" config object' });
1881
- } else {
1882
- const par = s.parallel;
1883
- if (!Array.isArray(par.steps)) {
1884
- errors.push({ path: `${path4}.parallel.steps`, message: 'Parallel must have a "steps" array' });
1885
- } else {
1886
- validateStepsArray(par.steps, `${path4}.parallel.steps`, errors);
1887
- }
2353
+ if (value === void 0) continue;
2354
+ if (fd.type === "string" && fd.required && typeof value !== "string") {
2355
+ errors.push({ path: fieldPath, message: `"${fieldName}" must be a string` });
1888
2356
  }
1889
- } else if (type === "file-read") {
1890
- if (!s.fileRead || typeof s.fileRead !== "object") {
1891
- errors.push({ path: `${path4}.fileRead`, message: 'File-read step must have a "fileRead" config object' });
1892
- } else {
1893
- const fr = s.fileRead;
1894
- if (!fr.path || typeof fr.path !== "string") {
1895
- errors.push({ path: `${path4}.fileRead.path`, message: 'File-read must have a string "path"' });
1896
- }
2357
+ if (fd.type === "number" && value !== void 0 && (typeof value !== "number" || value <= 0)) {
2358
+ errors.push({ path: fieldPath, message: `${fieldName} must be a positive number` });
1897
2359
  }
1898
- } else if (type === "file-write") {
1899
- if (!s.fileWrite || typeof s.fileWrite !== "object") {
1900
- errors.push({ path: `${path4}.fileWrite`, message: 'File-write step must have a "fileWrite" config object' });
1901
- } else {
1902
- const fw = s.fileWrite;
1903
- if (!fw.path || typeof fw.path !== "string") {
1904
- errors.push({ path: `${path4}.fileWrite.path`, message: 'File-write must have a string "path"' });
1905
- }
1906
- if (fw.content === void 0) {
1907
- errors.push({ path: `${path4}.fileWrite.content`, message: 'File-write must have "content"' });
1908
- }
2360
+ if (fd.type === "boolean" && value !== void 0 && typeof value !== "boolean") {
2361
+ errors.push({ path: fieldPath, message: `${fieldName} must be a boolean` });
1909
2362
  }
1910
- } else if (type === "while") {
1911
- if (!s.while || typeof s.while !== "object") {
1912
- errors.push({ path: `${path4}.while`, message: 'While step must have a "while" config object' });
1913
- } else {
1914
- const w = s.while;
1915
- if (!w.condition || typeof w.condition !== "string") {
1916
- errors.push({ path: `${path4}.while.condition`, message: 'While must have a string "condition"' });
1917
- }
1918
- if (!Array.isArray(w.steps)) {
1919
- errors.push({ path: `${path4}.while.steps`, message: 'While must have a "steps" array' });
2363
+ if (fd.stepsArray) {
2364
+ if (!Array.isArray(value)) {
2365
+ errors.push({ path: fieldPath, message: `"${fieldName}" must be a steps array` });
1920
2366
  } else {
1921
- validateStepsArray(w.steps, `${path4}.while.steps`, errors);
1922
- }
1923
- if (w.maxIterations !== void 0 && (typeof w.maxIterations !== "number" || w.maxIterations <= 0)) {
1924
- errors.push({ path: `${path4}.while.maxIterations`, message: "maxIterations must be a positive number" });
2367
+ validateStepsArray(value, fieldPath, errors);
1925
2368
  }
1926
2369
  }
1927
- } else if (type === "flow") {
1928
- if (!s.flow || typeof s.flow !== "object") {
1929
- errors.push({ path: `${path4}.flow`, message: 'Flow step must have a "flow" config object' });
1930
- } else {
1931
- const f = s.flow;
1932
- if (!f.key || typeof f.key !== "string") {
1933
- errors.push({ path: `${path4}.flow.key`, message: 'Flow must have a string "key"' });
1934
- }
1935
- if (f.inputs !== void 0 && (typeof f.inputs !== "object" || Array.isArray(f.inputs))) {
1936
- errors.push({ path: `${path4}.flow.inputs`, message: "Flow inputs must be an object" });
1937
- }
1938
- }
1939
- } else if (type === "paginate") {
1940
- if (!s.paginate || typeof s.paginate !== "object") {
1941
- errors.push({ path: `${path4}.paginate`, message: 'Paginate step must have a "paginate" config object' });
1942
- } else {
1943
- const p7 = s.paginate;
1944
- if (!p7.action || typeof p7.action !== "object") {
1945
- errors.push({ path: `${path4}.paginate.action`, message: 'Paginate must have an "action" config object' });
1946
- } else {
1947
- const a = p7.action;
1948
- if (!a.platform) errors.push({ path: `${path4}.paginate.action.platform`, message: 'Action must have "platform"' });
1949
- if (!a.actionId) errors.push({ path: `${path4}.paginate.action.actionId`, message: 'Action must have "actionId"' });
1950
- if (!a.connectionKey) errors.push({ path: `${path4}.paginate.action.connectionKey`, message: 'Action must have "connectionKey"' });
1951
- }
1952
- if (!p7.pageTokenField || typeof p7.pageTokenField !== "string") {
1953
- errors.push({ path: `${path4}.paginate.pageTokenField`, message: 'Paginate must have a string "pageTokenField"' });
1954
- }
1955
- if (!p7.resultsField || typeof p7.resultsField !== "string") {
1956
- errors.push({ path: `${path4}.paginate.resultsField`, message: 'Paginate must have a string "resultsField"' });
1957
- }
1958
- if (!p7.inputTokenParam || typeof p7.inputTokenParam !== "string") {
1959
- errors.push({ path: `${path4}.paginate.inputTokenParam`, message: 'Paginate must have a string "inputTokenParam"' });
1960
- }
1961
- if (p7.maxPages !== void 0 && (typeof p7.maxPages !== "number" || p7.maxPages <= 0)) {
1962
- errors.push({ path: `${path4}.paginate.maxPages`, message: "maxPages must be a positive number" });
1963
- }
1964
- }
1965
- } else if (type === "bash") {
1966
- if (!s.bash || typeof s.bash !== "object") {
1967
- errors.push({ path: `${path4}.bash`, message: 'Bash step must have a "bash" config object' });
1968
- } else {
1969
- const b = s.bash;
1970
- if (!b.command || typeof b.command !== "string") {
1971
- errors.push({ path: `${path4}.bash.command`, message: 'Bash must have a string "command"' });
1972
- }
1973
- if (b.timeout !== void 0 && (typeof b.timeout !== "number" || b.timeout <= 0)) {
1974
- errors.push({ path: `${path4}.bash.timeout`, message: "timeout must be a positive number" });
1975
- }
1976
- if (b.parseJson !== void 0 && typeof b.parseJson !== "boolean") {
1977
- errors.push({ path: `${path4}.bash.parseJson`, message: "parseJson must be a boolean" });
2370
+ if (descriptor.type === "paginate" && fieldName === "action") {
2371
+ if (typeof value === "object" && value !== null) {
2372
+ const a = value;
2373
+ if (!a.platform) errors.push({ path: `${fieldPath}.platform`, message: 'Action must have "platform"' });
2374
+ if (!a.actionId) errors.push({ path: `${fieldPath}.actionId`, message: 'Action must have "actionId"' });
2375
+ if (!a.connectionKey) errors.push({ path: `${fieldPath}.connectionKey`, message: 'Action must have "connectionKey"' });
1978
2376
  }
1979
2377
  }
1980
2378
  }
1981
2379
  }
1982
2380
  }
2381
+ function detectFlatConfigHint(step, descriptor) {
2382
+ const requiredFields = Object.entries(descriptor.fields).filter(([, fd]) => fd.required).map(([name]) => name);
2383
+ const flatFields = requiredFields.filter((f) => f in step);
2384
+ if (flatFields.length > 0) {
2385
+ return `. Hint: "${flatFields.join('", "')}" must be nested inside "${descriptor.configKey}": { ... }, not placed directly on the step`;
2386
+ }
2387
+ return "";
2388
+ }
2389
+ function capitalize(s) {
2390
+ return s.charAt(0).toUpperCase() + s.slice(1);
2391
+ }
1983
2392
  function validateStepIds(flow2) {
1984
2393
  const errors = [];
1985
2394
  const seen = /* @__PURE__ */ new Set();
2395
+ const nestedKeys = getNestedStepsKeys();
1986
2396
  function collectIds(steps, pathPrefix) {
1987
2397
  for (let i = 0; i < steps.length; i++) {
1988
2398
  const step = steps[i];
@@ -1992,13 +2402,12 @@ function validateStepIds(flow2) {
1992
2402
  } else {
1993
2403
  seen.add(step.id);
1994
2404
  }
1995
- if (step.condition) {
1996
- if (step.condition.then) collectIds(step.condition.then, `${path4}.condition.then`);
1997
- if (step.condition.else) collectIds(step.condition.else, `${path4}.condition.else`);
2405
+ for (const { configKey, fieldName } of nestedKeys) {
2406
+ const config = step[configKey];
2407
+ if (config && Array.isArray(config[fieldName])) {
2408
+ collectIds(config[fieldName], `${path4}.${configKey}.${fieldName}`);
2409
+ }
1998
2410
  }
1999
- if (step.loop?.steps) collectIds(step.loop.steps, `${path4}.loop.steps`);
2000
- if (step.parallel?.steps) collectIds(step.parallel.steps, `${path4}.parallel.steps`);
2001
- if (step.while?.steps) collectIds(step.while.steps, `${path4}.while.steps`);
2002
2411
  }
2003
2412
  }
2004
2413
  collectIds(flow2.steps, "steps");
@@ -2007,25 +2416,17 @@ function validateStepIds(flow2) {
2007
2416
  function validateSelectorReferences(flow2) {
2008
2417
  const errors = [];
2009
2418
  const inputNames = new Set(Object.keys(flow2.inputs));
2419
+ const nestedKeys = getNestedStepsKeys();
2010
2420
  function getAllStepIds(steps) {
2011
2421
  const ids = /* @__PURE__ */ new Set();
2012
2422
  for (const step of steps) {
2013
2423
  ids.add(step.id);
2014
- if (step.condition) {
2015
- for (const id of getAllStepIds(step.condition.then)) ids.add(id);
2016
- if (step.condition.else) {
2017
- for (const id of getAllStepIds(step.condition.else)) ids.add(id);
2424
+ for (const { configKey, fieldName } of nestedKeys) {
2425
+ const config = step[configKey];
2426
+ if (config && Array.isArray(config[fieldName])) {
2427
+ for (const id of getAllStepIds(config[fieldName])) ids.add(id);
2018
2428
  }
2019
2429
  }
2020
- if (step.loop?.steps) {
2021
- for (const id of getAllStepIds(step.loop.steps)) ids.add(id);
2022
- }
2023
- if (step.parallel?.steps) {
2024
- for (const id of getAllStepIds(step.parallel.steps)) ids.add(id);
2025
- }
2026
- if (step.while?.steps) {
2027
- for (const id of getAllStepIds(step.while.steps)) ids.add(id);
2028
- }
2029
2430
  }
2030
2431
  return ids;
2031
2432
  }
@@ -2072,49 +2473,29 @@ function validateSelectorReferences(flow2) {
2072
2473
  function checkStep(step, pathPrefix) {
2073
2474
  if (step.if) checkSelectors(extractSelectors(step.if), `${pathPrefix}.if`);
2074
2475
  if (step.unless) checkSelectors(extractSelectors(step.unless), `${pathPrefix}.unless`);
2075
- if (step.action) {
2076
- checkSelectors(extractSelectors(step.action), `${pathPrefix}.action`);
2077
- }
2078
- if (step.transform) {
2079
- }
2080
- if (step.condition) {
2081
- checkStep({ id: "__cond_expr", name: "", type: "transform", transform: { expression: "" } }, pathPrefix);
2082
- step.condition.then.forEach((s, i) => checkStep(s, `${pathPrefix}.condition.then[${i}]`));
2083
- step.condition.else?.forEach((s, i) => checkStep(s, `${pathPrefix}.condition.else[${i}]`));
2084
- }
2085
- if (step.loop) {
2086
- checkSelectors(extractSelectors(step.loop.over), `${pathPrefix}.loop.over`);
2087
- step.loop.steps.forEach((s, i) => checkStep(s, `${pathPrefix}.loop.steps[${i}]`));
2088
- }
2089
- if (step.parallel) {
2090
- step.parallel.steps.forEach((s, i) => checkStep(s, `${pathPrefix}.parallel.steps[${i}]`));
2091
- }
2092
- if (step.fileRead) {
2093
- checkSelectors(extractSelectors(step.fileRead.path), `${pathPrefix}.fileRead.path`);
2094
- }
2095
- if (step.fileWrite) {
2096
- checkSelectors(extractSelectors(step.fileWrite.path), `${pathPrefix}.fileWrite.path`);
2097
- checkSelectors(extractSelectors(step.fileWrite.content), `${pathPrefix}.fileWrite.content`);
2098
- }
2099
- if (step.while) {
2100
- step.while.steps.forEach((s, i) => checkStep(s, `${pathPrefix}.while.steps[${i}]`));
2101
- }
2102
- if (step.flow) {
2103
- checkSelectors(extractSelectors(step.flow.key), `${pathPrefix}.flow.key`);
2104
- if (step.flow.inputs) {
2105
- checkSelectors(extractSelectors(step.flow.inputs), `${pathPrefix}.flow.inputs`);
2106
- }
2107
- }
2108
- if (step.paginate) {
2109
- checkSelectors(extractSelectors(step.paginate.action), `${pathPrefix}.paginate.action`);
2110
- }
2111
- if (step.bash) {
2112
- checkSelectors(extractSelectors(step.bash.command), `${pathPrefix}.bash.command`);
2113
- if (step.bash.cwd) {
2114
- checkSelectors(extractSelectors(step.bash.cwd), `${pathPrefix}.bash.cwd`);
2476
+ const descriptor = getStepTypeDescriptor(step.type);
2477
+ if (descriptor) {
2478
+ const config = step[descriptor.configKey];
2479
+ if (config && typeof config === "object") {
2480
+ if (step.type !== "transform" && step.type !== "code") {
2481
+ for (const [fieldName, fd] of Object.entries(descriptor.fields)) {
2482
+ if (fd.stepsArray) continue;
2483
+ const value = config[fieldName];
2484
+ if (value !== void 0) {
2485
+ checkSelectors(extractSelectors(value), `${pathPrefix}.${descriptor.configKey}.${fieldName}`);
2486
+ }
2487
+ }
2488
+ }
2115
2489
  }
2116
- if (step.bash.env) {
2117
- checkSelectors(extractSelectors(step.bash.env), `${pathPrefix}.bash.env`);
2490
+ for (const { configKey, fieldName } of nestedKeys) {
2491
+ if (configKey === descriptor.configKey) {
2492
+ const c = step[configKey];
2493
+ if (c && Array.isArray(c[fieldName])) {
2494
+ c[fieldName].forEach(
2495
+ (s, i) => checkStep(s, `${pathPrefix}.${configKey}.${fieldName}[${i}]`)
2496
+ );
2497
+ }
2498
+ }
2118
2499
  }
2119
2500
  }
2120
2501
  }
@@ -2186,10 +2567,19 @@ async function flowCreateCommand(key, options) {
2186
2567
  intro2(pc7.bgCyan(pc7.black(" One Workflow ")));
2187
2568
  let flow2;
2188
2569
  if (options.definition) {
2570
+ let raw = options.definition;
2571
+ if (raw.startsWith("@")) {
2572
+ const filePath = raw.slice(1);
2573
+ try {
2574
+ raw = fs3.readFileSync(filePath, "utf-8");
2575
+ } catch (err) {
2576
+ error(`Cannot read file "${filePath}": ${err.message}`);
2577
+ }
2578
+ }
2189
2579
  try {
2190
- flow2 = JSON.parse(options.definition);
2580
+ flow2 = JSON.parse(raw);
2191
2581
  } catch {
2192
- error("Invalid JSON in --definition");
2582
+ error("Invalid JSON in --definition. If your JSON contains special characters (like :: in action IDs), try --definition @file.json instead.");
2193
2583
  }
2194
2584
  } else if (!process.stdin.isTTY) {
2195
2585
  const chunks = [];
@@ -2496,6 +2886,205 @@ function colorStatus(status) {
2496
2886
  return status;
2497
2887
  }
2498
2888
  }
2889
+ var SCAFFOLD_TEMPLATES = {
2890
+ basic: () => ({
2891
+ key: "my-workflow",
2892
+ name: "My Workflow",
2893
+ description: "A basic workflow with a single action step",
2894
+ version: "1",
2895
+ inputs: {
2896
+ connectionKey: {
2897
+ type: "string",
2898
+ required: true,
2899
+ description: "Connection key for the platform",
2900
+ connection: { platform: "PLATFORM_NAME" }
2901
+ }
2902
+ },
2903
+ steps: [
2904
+ {
2905
+ id: "step1",
2906
+ name: "Execute action",
2907
+ type: "action",
2908
+ action: {
2909
+ platform: "PLATFORM_NAME",
2910
+ actionId: "ACTION_ID_FROM_SEARCH",
2911
+ connectionKey: "$.input.connectionKey",
2912
+ data: {}
2913
+ }
2914
+ }
2915
+ ]
2916
+ }),
2917
+ conditional: () => ({
2918
+ key: "my-conditional-workflow",
2919
+ name: "Conditional Workflow",
2920
+ description: "Fetch data, then branch based on results",
2921
+ version: "1",
2922
+ inputs: {
2923
+ connectionKey: {
2924
+ type: "string",
2925
+ required: true,
2926
+ description: "Connection key",
2927
+ connection: { platform: "PLATFORM_NAME" }
2928
+ }
2929
+ },
2930
+ steps: [
2931
+ {
2932
+ id: "fetch",
2933
+ name: "Fetch data",
2934
+ type: "action",
2935
+ action: {
2936
+ platform: "PLATFORM_NAME",
2937
+ actionId: "ACTION_ID_FROM_SEARCH",
2938
+ connectionKey: "$.input.connectionKey"
2939
+ }
2940
+ },
2941
+ {
2942
+ id: "decide",
2943
+ name: "Check results",
2944
+ type: "condition",
2945
+ condition: {
2946
+ expression: "$.steps.fetch.response.data && $.steps.fetch.response.data.length > 0",
2947
+ then: [
2948
+ {
2949
+ id: "handleFound",
2950
+ name: "Handle found",
2951
+ type: "transform",
2952
+ transform: { expression: "$.steps.fetch.response.data[0]" }
2953
+ }
2954
+ ],
2955
+ else: [
2956
+ {
2957
+ id: "handleNotFound",
2958
+ name: "Handle not found",
2959
+ type: "transform",
2960
+ transform: { expression: "({ error: 'No results found' })" }
2961
+ }
2962
+ ]
2963
+ }
2964
+ }
2965
+ ]
2966
+ }),
2967
+ loop: () => ({
2968
+ key: "my-loop-workflow",
2969
+ name: "Loop Workflow",
2970
+ description: "Fetch a list, then process each item",
2971
+ version: "1",
2972
+ inputs: {
2973
+ connectionKey: {
2974
+ type: "string",
2975
+ required: true,
2976
+ description: "Connection key",
2977
+ connection: { platform: "PLATFORM_NAME" }
2978
+ }
2979
+ },
2980
+ steps: [
2981
+ {
2982
+ id: "fetchList",
2983
+ name: "Fetch items",
2984
+ type: "action",
2985
+ action: {
2986
+ platform: "PLATFORM_NAME",
2987
+ actionId: "ACTION_ID_FROM_SEARCH",
2988
+ connectionKey: "$.input.connectionKey"
2989
+ }
2990
+ },
2991
+ {
2992
+ id: "processItems",
2993
+ name: "Process each item",
2994
+ type: "loop",
2995
+ loop: {
2996
+ over: "$.steps.fetchList.response.data",
2997
+ as: "item",
2998
+ steps: [
2999
+ {
3000
+ id: "processItem",
3001
+ name: "Process single item",
3002
+ type: "transform",
3003
+ transform: { expression: "({ id: $.loop.item.id, processed: true })" }
3004
+ }
3005
+ ]
3006
+ }
3007
+ },
3008
+ {
3009
+ id: "summary",
3010
+ name: "Generate summary",
3011
+ type: "transform",
3012
+ transform: { expression: "({ total: $.steps.fetchList.response.data.length })" }
3013
+ }
3014
+ ]
3015
+ }),
3016
+ ai: () => ({
3017
+ key: "my-ai-workflow",
3018
+ name: "AI Analysis Workflow",
3019
+ description: "Fetch data, analyze with Claude, and send results",
3020
+ version: "1",
3021
+ inputs: {
3022
+ connectionKey: {
3023
+ type: "string",
3024
+ required: true,
3025
+ description: "Connection key for data source",
3026
+ connection: { platform: "PLATFORM_NAME" }
3027
+ }
3028
+ },
3029
+ steps: [
3030
+ {
3031
+ id: "fetchData",
3032
+ name: "Fetch raw data",
3033
+ type: "action",
3034
+ action: {
3035
+ platform: "PLATFORM_NAME",
3036
+ actionId: "ACTION_ID_FROM_SEARCH",
3037
+ connectionKey: "$.input.connectionKey"
3038
+ }
3039
+ },
3040
+ {
3041
+ id: "writeData",
3042
+ name: "Write data for analysis",
3043
+ type: "file-write",
3044
+ fileWrite: {
3045
+ path: "/tmp/workflow-data.json",
3046
+ content: "$.steps.fetchData.response"
3047
+ }
3048
+ },
3049
+ {
3050
+ id: "analyze",
3051
+ name: "Analyze with Claude",
3052
+ type: "bash",
3053
+ bash: {
3054
+ command: `cat /tmp/workflow-data.json | claude --print 'Analyze this data and return JSON with: {"summary": "...", "insights": [...], "recommendations": [...]}. Return ONLY valid JSON.' --output-format json`,
3055
+ timeout: 18e4,
3056
+ parseJson: true
3057
+ }
3058
+ },
3059
+ {
3060
+ id: "formatResult",
3061
+ name: "Format analysis output",
3062
+ type: "code",
3063
+ code: {
3064
+ source: "const a = $.steps.analyze.output;\nreturn {\n summary: a.summary,\n insights: a.insights,\n recommendations: a.recommendations\n};"
3065
+ }
3066
+ }
3067
+ ]
3068
+ })
3069
+ };
3070
+ async function flowScaffoldCommand(template) {
3071
+ const templateName = template || "basic";
3072
+ const templateFn = SCAFFOLD_TEMPLATES[templateName];
3073
+ if (!templateFn) {
3074
+ const available = Object.keys(SCAFFOLD_TEMPLATES).join(", ");
3075
+ if (isAgentMode()) {
3076
+ json({ error: `Unknown template "${templateName}". Available: ${available}` });
3077
+ process.exit(1);
3078
+ }
3079
+ error(`Unknown template "${templateName}". Available: ${available}`);
3080
+ }
3081
+ const scaffold = templateFn();
3082
+ if (isAgentMode()) {
3083
+ json(scaffold);
3084
+ return;
3085
+ }
3086
+ console.log(JSON.stringify(scaffold, null, 2));
3087
+ }
2499
3088
 
2500
3089
  // src/commands/relay.ts
2501
3090
  import pc8 from "picocolors";
@@ -2856,9 +3445,9 @@ one --agent <command>
2856
3445
 
2857
3446
  All commands return JSON. If an \`error\` key is present, the command failed.
2858
3447
 
2859
- ## IMPORTANT: Learn before you use
3448
+ ## IMPORTANT: Read the guide before you act
2860
3449
 
2861
- Before using any feature, you MUST read the corresponding skill documentation first. The skills are bundled with the CLI and teach you the correct workflow, required steps, template syntax, and common mistakes. Never guess \u2014 read the skill, then act.
3450
+ Before using any feature, read its guide section first: \`one guide actions\`, \`one guide flows\`, or \`one guide relay\`. The guide teaches you the correct workflow, required fields, and common mistakes. Never guess \u2014 read the guide, then act.
2862
3451
 
2863
3452
  ## Features
2864
3453
 
@@ -2996,91 +3585,7 @@ All errors return JSON: \`{"error": "message"}\`. Check the \`error\` key.
2996
3585
  - If search returns no results, try broader queries
2997
3586
  - Access control settings from \`one config\` may restrict execution
2998
3587
  `;
2999
- var GUIDE_FLOWS = `# One Flows \u2014 Reference
3000
-
3001
- ## Overview
3002
-
3003
- Workflows are JSON files at \`.one/flows/<key>.flow.json\` that chain actions across platforms.
3004
-
3005
- ## Commands
3006
-
3007
- \`\`\`bash
3008
- one --agent flow create <key> --definition '<json>' # Create
3009
- one --agent flow list # List
3010
- one --agent flow validate <key> # Validate
3011
- one --agent flow execute <key> -i name=value # Execute
3012
- one --agent flow execute <key> --dry-run --mock # Test with mock data
3013
- one --agent flow execute <key> --allow-bash # Enable bash steps
3014
- one --agent flow runs [flowKey] # List past runs
3015
- one --agent flow resume <runId> # Resume failed run
3016
- \`\`\`
3017
-
3018
- ## Building a Workflow
3019
-
3020
- 1. **Design first** \u2014 clarify the end goal, map the full value chain, identify where AI analysis is needed
3021
- 2. **Discover connections** \u2014 \`one --agent connection list\`
3022
- 3. **Get knowledge** for every action \u2014 \`one --agent actions knowledge <platform> <actionId>\`
3023
- 4. **Construct JSON** \u2014 declare inputs, wire steps with selectors
3024
- 5. **Validate** \u2014 \`one --agent flow validate <key>\`
3025
- 6. **Execute** \u2014 \`one --agent flow execute <key> -i param=value\`
3026
-
3027
- ## Step Types
3028
-
3029
- | Type | Purpose |
3030
- |------|---------|
3031
- | \`action\` | Execute a platform API action |
3032
- | \`transform\` | Single JS expression (implicit return) |
3033
- | \`code\` | Multi-line async JS (explicit return) |
3034
- | \`condition\` | If/then/else branching |
3035
- | \`loop\` | Iterate over array with optional concurrency |
3036
- | \`parallel\` | Run steps concurrently |
3037
- | \`file-read\` | Read file (optional JSON parse) |
3038
- | \`file-write\` | Write/append to file |
3039
- | \`while\` | Do-while loop with condition |
3040
- | \`flow\` | Execute a sub-flow |
3041
- | \`paginate\` | Auto-paginate API results |
3042
- | \`bash\` | Shell command (requires \`--allow-bash\`) |
3043
-
3044
- ## Selectors
3045
-
3046
- | Pattern | Resolves To |
3047
- |---------|-------------|
3048
- | \`$.input.paramName\` | Input value |
3049
- | \`$.steps.stepId.response\` | Full API response |
3050
- | \`$.steps.stepId.response.data[0].email\` | Nested field |
3051
- | \`$.steps.stepId.response.data[*].id\` | Wildcard array map |
3052
- | \`$.env.MY_VAR\` | Environment variable |
3053
- | \`$.loop.item\` / \`$.loop.i\` | Loop iteration |
3054
- | \`"Hello {{$.steps.getUser.response.name}}"\` | String interpolation |
3055
-
3056
- ## Error Handling
3057
-
3058
- \`\`\`json
3059
- {"onError": {"strategy": "retry", "retries": 3, "retryDelayMs": 1000}}
3060
- \`\`\`
3061
-
3062
- Strategies: \`fail\` (default), \`continue\`, \`retry\`, \`fallback\`
3063
-
3064
- Conditional execution: \`"if": "$.steps.prev.response.data.length > 0"\`
3065
-
3066
- ## AI-Augmented Pattern
3067
-
3068
- For workflows that need analysis/summarization, use the file-write \u2192 bash \u2192 code pattern:
3069
-
3070
- 1. \`file-write\` \u2014 save data to temp file
3071
- 2. \`bash\` \u2014 \`claude --print\` analyzes it (\`parseJson: true\`, \`timeout: 180000\`)
3072
- 3. \`code\` \u2014 parse and structure the output
3073
-
3074
- Set timeout to at least 180000ms (3 min). Run Claude-heavy flows sequentially, not in parallel.
3075
-
3076
- ## Notes
3077
-
3078
- - Connection keys are **inputs**, not hardcoded
3079
- - Action IDs in examples are placeholders \u2014 always use \`actions search\`
3080
- - Code steps allow \`crypto\`, \`buffer\`, \`url\`, \`path\` \u2014 \`fs\`, \`http\`, \`child_process\` are blocked
3081
- - Bash steps require \`--allow-bash\` flag
3082
- - State is persisted after every step \u2014 resume picks up where it left off
3083
- `;
3588
+ var GUIDE_FLOWS = generateFlowGuide();
3084
3589
  var GUIDE_RELAY = `# One Relay \u2014 Reference
3085
3590
 
3086
3591
  ## Overview
@@ -3745,6 +4250,9 @@ flow.command("resume <runId>").description("Resume a paused or failed workflow r
3745
4250
  flow.command("runs [flowKey]").description("List workflow runs (optionally filtered by flow key)").action(async (flowKey) => {
3746
4251
  await flowRunsCommand(flowKey);
3747
4252
  });
4253
+ flow.command("scaffold [template]").description("Generate a workflow scaffold (templates: basic, conditional, loop, ai)").action(async (template) => {
4254
+ await flowScaffoldCommand(template);
4255
+ });
3748
4256
  var relay = program.command("relay").alias("r").description("Receive webhooks from platforms and relay them via passthrough actions");
3749
4257
  relay.command("create").description("Create a new relay endpoint for a connection").requiredOption("--connection-key <key>", "Connection key for the source platform").option("--description <desc>", "Description of the relay endpoint").option("--event-filters <json>", `JSON array of event types to filter (e.g. '["customer.created"]')`).option("--tags <json>", "JSON array of tags").option("--create-webhook", "Automatically register the webhook with the source platform").action(async (options) => {
3750
4258
  await relayCreateCommand(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withone/cli",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "CLI for managing One",
5
5
  "type": "module",
6
6
  "files": [
@@ -19,6 +19,8 @@ description: |
19
19
 
20
20
  # One Workflows — Multi-Step API Workflows
21
21
 
22
+ <!-- Canonical flow schema: src/lib/flow-schema.ts (drives both validation and guide generation) -->
23
+
22
24
  You have access to the One CLI's workflow engine, which lets you create and execute multi-step API workflows as JSON files. Workflows chain actions across platforms — e.g., look up a Stripe customer, then send them a welcome email via Gmail.
23
25
 
24
26
  ## 1. Overview