@withone/cli 1.14.1 → 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
@@ -11,7 +11,7 @@ import {
11
11
  loadFlow,
12
12
  resolveFlowPath,
13
13
  saveFlow
14
- } from "./chunk-GEQRZGKM.js";
14
+ } from "./chunk-SX2Y4HDW.js";
15
15
 
16
16
  // src/index.ts
17
17
  import { createRequire as createRequire2 } from "module";
@@ -1316,7 +1316,9 @@ async function connectionListCommand(options) {
1316
1316
  connections: limited.map((conn) => ({
1317
1317
  platform: conn.platform,
1318
1318
  state: conn.state,
1319
- key: conn.key
1319
+ key: conn.key,
1320
+ ...conn.name && { name: conn.name },
1321
+ ...conn.tags?.length && { tags: conn.tags }
1320
1322
  })),
1321
1323
  ...limited.length < filtered.length && {
1322
1324
  hint: `Showing ${limited.length} of ${filtered.length} connections. Use --search <query> to filter by platform or --limit <n> to see more.`
@@ -1712,23 +1714,542 @@ function colorMethod(method) {
1712
1714
  // src/commands/flow.ts
1713
1715
  import pc7 from "picocolors";
1714
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
+
1715
2252
  // src/lib/flow-validator.ts
1716
- var VALID_STEP_TYPES = [
1717
- "action",
1718
- "transform",
1719
- "code",
1720
- "condition",
1721
- "loop",
1722
- "parallel",
1723
- "file-read",
1724
- "file-write",
1725
- "while",
1726
- "flow",
1727
- "paginate",
1728
- "bash"
1729
- ];
1730
- var VALID_INPUT_TYPES = ["string", "number", "boolean", "object", "array"];
1731
- var VALID_ERROR_STRATEGIES = ["fail", "continue", "retry", "fallback"];
1732
2253
  function validateFlowSchema(flow2) {
1733
2254
  const errors = [];
1734
2255
  if (!flow2 || typeof flow2 !== "object") {
@@ -1738,7 +2259,7 @@ function validateFlowSchema(flow2) {
1738
2259
  const f = flow2;
1739
2260
  if (!f.key || typeof f.key !== "string") {
1740
2261
  errors.push({ path: "key", message: 'Flow must have a string "key"' });
1741
- } 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) {
1742
2263
  errors.push({ path: "key", message: "Flow key must be kebab-case (lowercase letters, numbers, hyphens)" });
1743
2264
  }
1744
2265
  if (!f.name || typeof f.name !== "string") {
@@ -1761,8 +2282,8 @@ function validateFlowSchema(flow2) {
1761
2282
  continue;
1762
2283
  }
1763
2284
  const d = decl;
1764
- if (!d.type || !VALID_INPUT_TYPES.includes(d.type)) {
1765
- 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(", ")}` });
1766
2287
  }
1767
2288
  if (d.connection !== void 0) {
1768
2289
  if (!d.connection || typeof d.connection !== "object") {
@@ -1784,6 +2305,7 @@ function validateFlowSchema(flow2) {
1784
2305
  return errors;
1785
2306
  }
1786
2307
  function validateStepsArray(steps, pathPrefix, errors) {
2308
+ const validTypes = FLOW_SCHEMA.stepTypes.map((st) => st.type);
1787
2309
  for (let i = 0; i < steps.length; i++) {
1788
2310
  const step = steps[i];
1789
2311
  const path4 = `${pathPrefix}[${i}]`;
@@ -1798,189 +2320,79 @@ function validateStepsArray(steps, pathPrefix, errors) {
1798
2320
  if (!s.name || typeof s.name !== "string") {
1799
2321
  errors.push({ path: `${path4}.name`, message: 'Step must have a string "name"' });
1800
2322
  }
1801
- if (!s.type || !VALID_STEP_TYPES.includes(s.type)) {
1802
- 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;
1803
2326
  }
1804
2327
  if (s.onError && typeof s.onError === "object") {
1805
2328
  const oe = s.onError;
1806
- if (!VALID_ERROR_STRATEGIES.includes(oe.strategy)) {
1807
- 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(", ")}` });
1808
2331
  }
1809
2332
  }
1810
- const type = s.type;
1811
- if (type === "action") {
1812
- if (!s.action || typeof s.action !== "object") {
1813
- errors.push({ path: `${path4}.action`, message: 'Action step must have an "action" config object' });
1814
- } else {
1815
- const a = s.action;
1816
- if (!a.platform) errors.push({ path: `${path4}.action.platform`, message: 'Action must have "platform"' });
1817
- if (!a.actionId) errors.push({ path: `${path4}.action.actionId`, message: 'Action must have "actionId"' });
1818
- if (!a.connectionKey) errors.push({ path: `${path4}.action.connectionKey`, message: 'Action must have "connectionKey"' });
1819
- }
1820
- } else if (type === "transform") {
1821
- if (!s.transform || typeof s.transform !== "object") {
1822
- errors.push({ path: `${path4}.transform`, message: 'Transform step must have a "transform" config object' });
1823
- } else {
1824
- const t = s.transform;
1825
- if (!t.expression || typeof t.expression !== "string") {
1826
- errors.push({ path: `${path4}.transform.expression`, message: 'Transform must have a string "expression"' });
1827
- }
1828
- }
1829
- } else if (type === "code") {
1830
- if (!s.code || typeof s.code !== "object") {
1831
- errors.push({ path: `${path4}.code`, message: 'Code step must have a "code" config object' });
1832
- } else {
1833
- const c = s.code;
1834
- if (!c.source || typeof c.source !== "string") {
1835
- errors.push({ path: `${path4}.code.source`, message: 'Code must have a string "source"' });
1836
- }
1837
- }
1838
- } else if (type === "condition") {
1839
- if (!s.condition || typeof s.condition !== "object") {
1840
- errors.push({ path: `${path4}.condition`, message: 'Condition step must have a "condition" config object' });
1841
- } else {
1842
- const c = s.condition;
1843
- if (!c.expression || typeof c.expression !== "string") {
1844
- errors.push({ path: `${path4}.condition.expression`, message: 'Condition must have a string "expression"' });
1845
- }
1846
- if (!Array.isArray(c.then)) {
1847
- errors.push({ path: `${path4}.condition.then`, message: 'Condition must have a "then" steps array' });
1848
- } else {
1849
- validateStepsArray(c.then, `${path4}.condition.then`, errors);
1850
- }
1851
- if (c.else !== void 0) {
1852
- if (!Array.isArray(c.else)) {
1853
- errors.push({ path: `${path4}.condition.else`, message: 'Condition "else" must be a steps array' });
1854
- } else {
1855
- validateStepsArray(c.else, `${path4}.condition.else`, errors);
1856
- }
1857
- }
1858
- }
1859
- } else if (type === "loop") {
1860
- if (!s.loop || typeof s.loop !== "object") {
1861
- errors.push({ path: `${path4}.loop`, message: 'Loop step must have a "loop" config object' });
1862
- } else {
1863
- const l = s.loop;
1864
- if (!l.over || typeof l.over !== "string") {
1865
- errors.push({ path: `${path4}.loop.over`, message: 'Loop must have a string "over" selector' });
1866
- }
1867
- if (!l.as || typeof l.as !== "string") {
1868
- errors.push({ path: `${path4}.loop.as`, message: 'Loop must have a string "as" variable name' });
1869
- }
1870
- if (!Array.isArray(l.steps)) {
1871
- errors.push({ path: `${path4}.loop.steps`, message: 'Loop must have a "steps" array' });
1872
- } else {
1873
- validateStepsArray(l.steps, `${path4}.loop.steps`, errors);
1874
- }
1875
- }
1876
- } else if (type === "parallel") {
1877
- if (!s.parallel || typeof s.parallel !== "object") {
1878
- errors.push({ path: `${path4}.parallel`, message: 'Parallel step must have a "parallel" config object' });
1879
- } else {
1880
- const par = s.parallel;
1881
- if (!Array.isArray(par.steps)) {
1882
- errors.push({ path: `${path4}.parallel.steps`, message: 'Parallel must have a "steps" array' });
1883
- } else {
1884
- validateStepsArray(par.steps, `${path4}.parallel.steps`, errors);
1885
- }
1886
- }
1887
- } else if (type === "file-read") {
1888
- if (!s.fileRead || typeof s.fileRead !== "object") {
1889
- errors.push({ path: `${path4}.fileRead`, message: 'File-read step must have a "fileRead" config object' });
1890
- } else {
1891
- const fr = s.fileRead;
1892
- if (!fr.path || typeof fr.path !== "string") {
1893
- errors.push({ path: `${path4}.fileRead.path`, message: 'File-read must have a string "path"' });
1894
- }
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;
1895
2352
  }
1896
- } else if (type === "file-write") {
1897
- if (!s.fileWrite || typeof s.fileWrite !== "object") {
1898
- errors.push({ path: `${path4}.fileWrite`, message: 'File-write step must have a "fileWrite" config object' });
1899
- } else {
1900
- const fw = s.fileWrite;
1901
- if (!fw.path || typeof fw.path !== "string") {
1902
- errors.push({ path: `${path4}.fileWrite.path`, message: 'File-write must have a string "path"' });
1903
- }
1904
- if (fw.content === void 0) {
1905
- errors.push({ path: `${path4}.fileWrite.content`, message: 'File-write must have "content"' });
1906
- }
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` });
1907
2356
  }
1908
- } else if (type === "while") {
1909
- if (!s.while || typeof s.while !== "object") {
1910
- errors.push({ path: `${path4}.while`, message: 'While step must have a "while" config object' });
1911
- } else {
1912
- const w = s.while;
1913
- if (!w.condition || typeof w.condition !== "string") {
1914
- errors.push({ path: `${path4}.while.condition`, message: 'While must have a string "condition"' });
1915
- }
1916
- if (!Array.isArray(w.steps)) {
1917
- errors.push({ path: `${path4}.while.steps`, message: 'While must have a "steps" array' });
1918
- } else {
1919
- validateStepsArray(w.steps, `${path4}.while.steps`, errors);
1920
- }
1921
- if (w.maxIterations !== void 0 && (typeof w.maxIterations !== "number" || w.maxIterations <= 0)) {
1922
- errors.push({ path: `${path4}.while.maxIterations`, message: "maxIterations must be a positive number" });
1923
- }
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` });
1924
2359
  }
1925
- } else if (type === "flow") {
1926
- if (!s.flow || typeof s.flow !== "object") {
1927
- errors.push({ path: `${path4}.flow`, message: 'Flow step must have a "flow" config object' });
1928
- } else {
1929
- const f = s.flow;
1930
- if (!f.key || typeof f.key !== "string") {
1931
- errors.push({ path: `${path4}.flow.key`, message: 'Flow must have a string "key"' });
1932
- }
1933
- if (f.inputs !== void 0 && (typeof f.inputs !== "object" || Array.isArray(f.inputs))) {
1934
- errors.push({ path: `${path4}.flow.inputs`, message: "Flow inputs must be an object" });
1935
- }
2360
+ if (fd.type === "boolean" && value !== void 0 && typeof value !== "boolean") {
2361
+ errors.push({ path: fieldPath, message: `${fieldName} must be a boolean` });
1936
2362
  }
1937
- } else if (type === "paginate") {
1938
- if (!s.paginate || typeof s.paginate !== "object") {
1939
- errors.push({ path: `${path4}.paginate`, message: 'Paginate step must have a "paginate" config object' });
1940
- } else {
1941
- const p7 = s.paginate;
1942
- if (!p7.action || typeof p7.action !== "object") {
1943
- errors.push({ path: `${path4}.paginate.action`, message: 'Paginate must have an "action" config object' });
2363
+ if (fd.stepsArray) {
2364
+ if (!Array.isArray(value)) {
2365
+ errors.push({ path: fieldPath, message: `"${fieldName}" must be a steps array` });
1944
2366
  } else {
1945
- const a = p7.action;
1946
- if (!a.platform) errors.push({ path: `${path4}.paginate.action.platform`, message: 'Action must have "platform"' });
1947
- if (!a.actionId) errors.push({ path: `${path4}.paginate.action.actionId`, message: 'Action must have "actionId"' });
1948
- if (!a.connectionKey) errors.push({ path: `${path4}.paginate.action.connectionKey`, message: 'Action must have "connectionKey"' });
1949
- }
1950
- if (!p7.pageTokenField || typeof p7.pageTokenField !== "string") {
1951
- errors.push({ path: `${path4}.paginate.pageTokenField`, message: 'Paginate must have a string "pageTokenField"' });
1952
- }
1953
- if (!p7.resultsField || typeof p7.resultsField !== "string") {
1954
- errors.push({ path: `${path4}.paginate.resultsField`, message: 'Paginate must have a string "resultsField"' });
1955
- }
1956
- if (!p7.inputTokenParam || typeof p7.inputTokenParam !== "string") {
1957
- errors.push({ path: `${path4}.paginate.inputTokenParam`, message: 'Paginate must have a string "inputTokenParam"' });
1958
- }
1959
- if (p7.maxPages !== void 0 && (typeof p7.maxPages !== "number" || p7.maxPages <= 0)) {
1960
- errors.push({ path: `${path4}.paginate.maxPages`, message: "maxPages must be a positive number" });
2367
+ validateStepsArray(value, fieldPath, errors);
1961
2368
  }
1962
2369
  }
1963
- } else if (type === "bash") {
1964
- if (!s.bash || typeof s.bash !== "object") {
1965
- errors.push({ path: `${path4}.bash`, message: 'Bash step must have a "bash" config object' });
1966
- } else {
1967
- const b = s.bash;
1968
- if (!b.command || typeof b.command !== "string") {
1969
- errors.push({ path: `${path4}.bash.command`, message: 'Bash must have a string "command"' });
1970
- }
1971
- if (b.timeout !== void 0 && (typeof b.timeout !== "number" || b.timeout <= 0)) {
1972
- errors.push({ path: `${path4}.bash.timeout`, message: "timeout must be a positive number" });
1973
- }
1974
- if (b.parseJson !== void 0 && typeof b.parseJson !== "boolean") {
1975
- 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"' });
1976
2376
  }
1977
2377
  }
1978
2378
  }
1979
2379
  }
1980
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
+ }
1981
2392
  function validateStepIds(flow2) {
1982
2393
  const errors = [];
1983
2394
  const seen = /* @__PURE__ */ new Set();
2395
+ const nestedKeys = getNestedStepsKeys();
1984
2396
  function collectIds(steps, pathPrefix) {
1985
2397
  for (let i = 0; i < steps.length; i++) {
1986
2398
  const step = steps[i];
@@ -1990,13 +2402,12 @@ function validateStepIds(flow2) {
1990
2402
  } else {
1991
2403
  seen.add(step.id);
1992
2404
  }
1993
- if (step.condition) {
1994
- if (step.condition.then) collectIds(step.condition.then, `${path4}.condition.then`);
1995
- 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
+ }
1996
2410
  }
1997
- if (step.loop?.steps) collectIds(step.loop.steps, `${path4}.loop.steps`);
1998
- if (step.parallel?.steps) collectIds(step.parallel.steps, `${path4}.parallel.steps`);
1999
- if (step.while?.steps) collectIds(step.while.steps, `${path4}.while.steps`);
2000
2411
  }
2001
2412
  }
2002
2413
  collectIds(flow2.steps, "steps");
@@ -2005,25 +2416,17 @@ function validateStepIds(flow2) {
2005
2416
  function validateSelectorReferences(flow2) {
2006
2417
  const errors = [];
2007
2418
  const inputNames = new Set(Object.keys(flow2.inputs));
2419
+ const nestedKeys = getNestedStepsKeys();
2008
2420
  function getAllStepIds(steps) {
2009
2421
  const ids = /* @__PURE__ */ new Set();
2010
2422
  for (const step of steps) {
2011
2423
  ids.add(step.id);
2012
- if (step.condition) {
2013
- for (const id of getAllStepIds(step.condition.then)) ids.add(id);
2014
- if (step.condition.else) {
2015
- 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);
2016
2428
  }
2017
2429
  }
2018
- if (step.loop?.steps) {
2019
- for (const id of getAllStepIds(step.loop.steps)) ids.add(id);
2020
- }
2021
- if (step.parallel?.steps) {
2022
- for (const id of getAllStepIds(step.parallel.steps)) ids.add(id);
2023
- }
2024
- if (step.while?.steps) {
2025
- for (const id of getAllStepIds(step.while.steps)) ids.add(id);
2026
- }
2027
2430
  }
2028
2431
  return ids;
2029
2432
  }
@@ -2070,49 +2473,29 @@ function validateSelectorReferences(flow2) {
2070
2473
  function checkStep(step, pathPrefix) {
2071
2474
  if (step.if) checkSelectors(extractSelectors(step.if), `${pathPrefix}.if`);
2072
2475
  if (step.unless) checkSelectors(extractSelectors(step.unless), `${pathPrefix}.unless`);
2073
- if (step.action) {
2074
- checkSelectors(extractSelectors(step.action), `${pathPrefix}.action`);
2075
- }
2076
- if (step.transform) {
2077
- }
2078
- if (step.condition) {
2079
- checkStep({ id: "__cond_expr", name: "", type: "transform", transform: { expression: "" } }, pathPrefix);
2080
- step.condition.then.forEach((s, i) => checkStep(s, `${pathPrefix}.condition.then[${i}]`));
2081
- step.condition.else?.forEach((s, i) => checkStep(s, `${pathPrefix}.condition.else[${i}]`));
2082
- }
2083
- if (step.loop) {
2084
- checkSelectors(extractSelectors(step.loop.over), `${pathPrefix}.loop.over`);
2085
- step.loop.steps.forEach((s, i) => checkStep(s, `${pathPrefix}.loop.steps[${i}]`));
2086
- }
2087
- if (step.parallel) {
2088
- step.parallel.steps.forEach((s, i) => checkStep(s, `${pathPrefix}.parallel.steps[${i}]`));
2089
- }
2090
- if (step.fileRead) {
2091
- checkSelectors(extractSelectors(step.fileRead.path), `${pathPrefix}.fileRead.path`);
2092
- }
2093
- if (step.fileWrite) {
2094
- checkSelectors(extractSelectors(step.fileWrite.path), `${pathPrefix}.fileWrite.path`);
2095
- checkSelectors(extractSelectors(step.fileWrite.content), `${pathPrefix}.fileWrite.content`);
2096
- }
2097
- if (step.while) {
2098
- step.while.steps.forEach((s, i) => checkStep(s, `${pathPrefix}.while.steps[${i}]`));
2099
- }
2100
- if (step.flow) {
2101
- checkSelectors(extractSelectors(step.flow.key), `${pathPrefix}.flow.key`);
2102
- if (step.flow.inputs) {
2103
- checkSelectors(extractSelectors(step.flow.inputs), `${pathPrefix}.flow.inputs`);
2104
- }
2105
- }
2106
- if (step.paginate) {
2107
- checkSelectors(extractSelectors(step.paginate.action), `${pathPrefix}.paginate.action`);
2108
- }
2109
- if (step.bash) {
2110
- checkSelectors(extractSelectors(step.bash.command), `${pathPrefix}.bash.command`);
2111
- if (step.bash.cwd) {
2112
- 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
+ }
2113
2489
  }
2114
- if (step.bash.env) {
2115
- 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
+ }
2116
2499
  }
2117
2500
  }
2118
2501
  }
@@ -2184,10 +2567,19 @@ async function flowCreateCommand(key, options) {
2184
2567
  intro2(pc7.bgCyan(pc7.black(" One Workflow ")));
2185
2568
  let flow2;
2186
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
+ }
2187
2579
  try {
2188
- flow2 = JSON.parse(options.definition);
2580
+ flow2 = JSON.parse(raw);
2189
2581
  } catch {
2190
- 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.");
2191
2583
  }
2192
2584
  } else if (!process.stdin.isTTY) {
2193
2585
  const chunks = [];
@@ -2494,909 +2886,791 @@ function colorStatus(status) {
2494
2886
  return status;
2495
2887
  }
2496
2888
  }
2497
-
2498
- // src/commands/guide.ts
2499
- import pc8 from "picocolors";
2500
-
2501
- // src/lib/guide-content.ts
2502
- var GUIDE_OVERVIEW = `# One CLI \u2014 Agent Guide
2503
-
2504
- ## Setup
2505
-
2506
- 1. Run \`one init\` to configure your API key
2507
- 2. Run \`one add <platform>\` to connect platforms via OAuth
2508
- 3. Run \`one --agent connection list\` to verify connections
2509
-
2510
- ## The --agent Flag
2511
-
2512
- Always use \`--agent\` for machine-readable JSON output. It disables colors, spinners, and interactive prompts.
2513
-
2514
- \`\`\`bash
2515
- one --agent <command>
2516
- \`\`\`
2517
-
2518
- All commands return JSON. If an \`error\` key is present, the command failed.
2519
-
2520
- ## Topics
2521
-
2522
- This guide has three sections you can request individually:
2523
-
2524
- - **overview** \u2014 This section. Setup, flag usage, and discovery workflow.
2525
- - **actions** \u2014 Full workflow for searching, reading docs, and executing platform actions.
2526
- - **workflows** \u2014 Building and executing multi-step API workflows (JSON-based).
2527
-
2528
- ## Discovery Workflow
2529
-
2530
- 1. \`one --agent connection list\` \u2014 See connected platforms and connection keys
2531
- 2. \`one --agent actions search <platform> <query>\` \u2014 Find actions
2532
- 3. \`one --agent actions knowledge <platform> <actionId>\` \u2014 Read full docs (REQUIRED before execute)
2533
- 4. \`one --agent actions execute <platform> <actionId> <connectionKey>\` \u2014 Execute the action
2534
-
2535
- For multi-step workflows:
2536
- 1. Discover actions with the workflow above
2537
- 2. Build a workflow JSON definition
2538
- 3. \`one --agent flow create <key> --definition '<json>'\`
2539
- 4. \`one --agent flow execute <key> -i param=value\`
2540
-
2541
- Platform names are always kebab-case (e.g., \`hub-spot\`, \`google-calendar\`).
2542
- Run \`one platforms\` to browse all 200+ available platforms.
2543
- `;
2544
- var GUIDE_ACTIONS = `# One Actions CLI Workflow
2545
-
2546
- You have access to the One CLI which lets you interact with 200+ third-party platforms through their APIs. The CLI handles authentication, request building, and execution through One's passthrough proxy.
2547
-
2548
- ## The Workflow
2549
-
2550
- Always follow this sequence \u2014 each step builds on the previous one:
2551
-
2552
- 1. **List connections** to see what platforms the user has connected
2553
- 2. **Search actions** to find the right API action for what the user wants to do
2554
- 3. **Get knowledge** to understand the action's parameters, requirements, and structure
2555
- 4. **Execute** the action with the correct parameters
2556
-
2557
- Never skip the knowledge step before executing \u2014 it contains critical information about required parameters, validation rules, and request structure that you need to build a correct request.
2558
-
2559
- ## Commands
2560
-
2561
- ### 1. List Connections
2562
-
2563
- \`\`\`bash
2564
- one --agent connection list
2565
- \`\`\`
2566
-
2567
- Returns JSON with all connected platforms, their status, and connection keys. You need the **connection key** for executing actions, and the **platform name** (kebab-case) for searching actions.
2568
-
2569
- Output format:
2570
- \`\`\`json
2571
- {"connections": [{"platform": "gmail", "state": "active", "key": "conn_abc123"}, ...]}
2572
- \`\`\`
2573
-
2574
- ### 2. Search Actions
2575
-
2576
- \`\`\`bash
2577
- one --agent actions search <platform> <query>
2578
- \`\`\`
2579
-
2580
- Search for actions on a specific platform using natural language. Returns JSON with up to 5 matching actions including their action IDs, HTTP methods, and paths.
2581
-
2582
- - \`<platform>\` \u2014 Platform name in kebab-case exactly as shown in the connections list (e.g., \`gmail\`, \`shopify\`, \`hub-spot\`)
2583
- - \`<query>\` \u2014 Natural language description of what you want to do (e.g., \`"send email"\`, \`"list contacts"\`, \`"create order"\`)
2584
-
2585
- Options:
2586
- - \`-t, --type <execute|knowledge>\` \u2014 Use \`execute\` when the user wants to perform an action, \`knowledge\` when they want documentation or want to write code. Defaults to \`knowledge\`.
2587
-
2588
- Example:
2589
- \`\`\`bash
2590
- one --agent actions search gmail "send email" -t execute
2591
- \`\`\`
2592
-
2593
- Output format:
2594
- \`\`\`json
2595
- {"actions": [{"_id": "abc123", "title": "Send Email", "tags": [...], "method": "POST", "path": "/messages/send"}, ...]}
2596
- \`\`\`
2597
-
2598
- ### 3. Get Action Knowledge
2599
-
2600
- \`\`\`bash
2601
- one --agent actions knowledge <platform> <actionId>
2602
- \`\`\`
2603
-
2604
- Get comprehensive documentation for an action including parameters, requirements, validation rules, request/response structure, and examples. Returns JSON with the full API knowledge and HTTP method.
2605
-
2606
- Always call this before executing \u2014 it tells you exactly what parameters are required and how to structure the request.
2607
-
2608
- Example:
2609
- \`\`\`bash
2610
- one --agent actions knowledge gmail 67890abcdef
2611
- \`\`\`
2612
-
2613
- Output format:
2614
- \`\`\`json
2615
- {"knowledge": "...full API documentation and guidance...", "method": "POST"}
2616
- \`\`\`
2617
-
2618
- ### 4. Execute Action
2619
-
2620
- \`\`\`bash
2621
- one --agent actions execute <platform> <actionId> <connectionKey> [options]
2622
- \`\`\`
2623
-
2624
- Execute an action on a connected platform. Returns JSON with the request details and response data. You must have retrieved the knowledge for this action first.
2625
-
2626
- - \`<platform>\` \u2014 Platform name in kebab-case
2627
- - \`<actionId>\` \u2014 Action ID from the search results
2628
- - \`<connectionKey>\` \u2014 Connection key from \`one connection list\`
2629
-
2630
- Options:
2631
- - \`-d, --data <json>\` \u2014 Request body as JSON string (for POST, PUT, PATCH)
2632
- - \`--path-vars <json>\` \u2014 Path variables as JSON (for URLs with \`{id}\` placeholders)
2633
- - \`--query-params <json>\` \u2014 Query parameters as JSON
2634
- - \`--headers <json>\` \u2014 Additional headers as JSON
2635
- - \`--form-data\` \u2014 Send as multipart/form-data instead of JSON
2636
- - \`--form-url-encoded\` \u2014 Send as application/x-www-form-urlencoded
2637
- - \`--dry-run\` \u2014 Show the request that would be sent without executing it
2638
-
2639
- Examples:
2640
- \`\`\`bash
2641
- # Simple GET request
2642
- one --agent actions execute shopify <actionId> <connectionKey>
2643
-
2644
- # POST with data
2645
- one --agent actions execute hub-spot <actionId> <connectionKey> \\
2646
- -d '{"properties": {"email": "jane@example.com", "firstname": "Jane"}}'
2647
-
2648
- # With path variables and query params
2649
- one --agent actions execute shopify <actionId> <connectionKey> \\
2650
- --path-vars '{"order_id": "12345"}' \\
2651
- --query-params '{"limit": "10"}'
2652
- \`\`\`
2653
-
2654
- Output format:
2655
- \`\`\`json
2656
- {"request": {"method": "POST", "url": "https://..."}, "response": {...}}
2657
- \`\`\`
2658
-
2659
- ## Error Handling
2660
-
2661
- All errors return JSON in agent mode:
2662
- \`\`\`json
2663
- {"error": "Error message here"}
2664
- \`\`\`
2665
-
2666
- Parse the output as JSON. If the \`error\` key is present, the command failed \u2014 report the error message to the user.
2667
-
2668
- ## Important Notes
2669
-
2670
- - **Always use \`--agent\` flag** \u2014 it produces structured JSON output without spinners, colors, or interactive prompts
2671
- - Platform names are always **kebab-case** (e.g., \`hub-spot\` not \`HubSpot\`, \`ship-station\` not \`ShipStation\`)
2672
- - Always use the **exact action ID** from search results \u2014 don't guess or construct them
2673
- - Always read the knowledge output carefully \u2014 it tells you which parameters are required vs optional, what format they need to be in, and any caveats specific to that API
2674
- - JSON values passed to \`-d\`, \`--path-vars\`, \`--query-params\`, and \`--headers\` must be valid JSON strings (use single quotes around the JSON to avoid shell escaping issues)
2675
- - If search returns no results, try broader queries (e.g., \`"list"\` instead of \`"list active premium customers"\`)
2676
- - The execute command respects access control settings configured via \`one config\` \u2014 if execution is blocked, the user may need to adjust their permissions
2677
- `;
2678
- var GUIDE_FLOWS = `# One Workflows \u2014 Multi-Step API Workflows
2679
-
2680
- 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 \u2014 e.g., look up a Stripe customer, then send them a welcome email via Gmail.
2681
-
2682
- ## 1. Overview
2683
-
2684
- - Workflows are JSON files stored at \`.one/flows/<key>.flow.json\`
2685
- - All dynamic values (including connection keys) are declared as **inputs**
2686
- - Each workflow has a unique **key** used to reference and execute it
2687
- - Executed via \`one --agent flow execute <key> -i name=value\`
2688
-
2689
- ## 2. Building a Workflow \u2014 Step-by-Step Process
2690
-
2691
- **You MUST follow this process to build a correct workflow:**
2692
-
2693
- ### Step 1: Discover connections
2694
-
2695
- \`\`\`bash
2696
- one --agent connection list
2697
- \`\`\`
2698
-
2699
- Find out which platforms are connected and get their connection keys.
2700
-
2701
- ### Step 2: For EACH API action needed, get the knowledge
2702
-
2703
- \`\`\`bash
2704
- # Find the action ID
2705
- one --agent actions search <platform> "<query>" -t execute
2706
-
2707
- # Read the full docs \u2014 REQUIRED before adding to a workflow
2708
- one --agent actions knowledge <platform> <actionId>
2709
- \`\`\`
2710
-
2711
- **CRITICAL:** You MUST call \`one actions knowledge\` for every action you include in the workflow. The knowledge output tells you the exact request body structure, required fields, path variables, and query parameters. Without this, your workflow JSON will have incorrect data shapes.
2712
-
2713
- ### Step 3: Construct the workflow JSON
2714
-
2715
- Using the knowledge gathered, build the workflow JSON with:
2716
- - All inputs declared (connection keys + user parameters)
2717
- - Each step with the correct actionId, platform, and data structure (from knowledge)
2718
- - Data wired between steps using \`$.input.*\` and \`$.steps.*\` selectors
2719
-
2720
- ### Step 4: Write the workflow file
2721
-
2722
- \`\`\`bash
2723
- one --agent flow create <key> --definition '<json>'
2724
- \`\`\`
2725
-
2726
- Or write directly to \`.one/flows/<key>.flow.json\`.
2727
-
2728
- ### Step 5: Validate
2729
-
2730
- \`\`\`bash
2731
- one --agent flow validate <key>
2732
- \`\`\`
2733
-
2734
- ### Step 6: Execute
2735
-
2736
- \`\`\`bash
2737
- one --agent flow execute <key> -i connectionKey=xxx -i param=value
2738
- \`\`\`
2739
-
2740
- ## 3. Workflow JSON Schema Reference
2741
-
2742
- \`\`\`json
2743
- {
2744
- "key": "welcome-customer",
2745
- "name": "Welcome New Customer",
2746
- "description": "Look up a Stripe customer and send them a welcome email via Gmail",
2747
- "version": "1",
2748
- "inputs": {
2749
- "stripeConnectionKey": {
2750
- "type": "string",
2751
- "required": true,
2752
- "description": "Stripe connection key from one connection list",
2753
- "connection": { "platform": "stripe" }
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
+ }
2754
2902
  },
2755
- "gmailConnectionKey": {
2756
- "type": "string",
2757
- "required": true,
2758
- "description": "Gmail connection key from one connection list",
2759
- "connection": { "platform": "gmail" }
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
+ }
2760
2929
  },
2761
- "customerEmail": {
2762
- "type": "string",
2763
- "required": true,
2764
- "description": "Customer email to look up"
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
+ }
3088
+
3089
+ // src/commands/relay.ts
3090
+ import pc8 from "picocolors";
3091
+ function getConfig3() {
3092
+ const apiKey = getApiKey();
3093
+ if (!apiKey) {
3094
+ error("Not configured. Run `one init` first.");
3095
+ }
3096
+ const ac = getAccessControlFromAllSources();
3097
+ const connectionKeys = ac.connectionKeys || ["*"];
3098
+ return { apiKey, connectionKeys };
3099
+ }
3100
+ function parseJsonArg2(value, argName) {
3101
+ try {
3102
+ return JSON.parse(value);
3103
+ } catch {
3104
+ error(`Invalid JSON for ${argName}: ${value}`);
3105
+ }
3106
+ }
3107
+ async function relayCreateCommand(options) {
3108
+ const { apiKey, connectionKeys } = getConfig3();
3109
+ if (!connectionKeys.includes("*") && !connectionKeys.includes(options.connectionKey)) {
3110
+ error(`Connection key "${options.connectionKey}" is not allowed.`);
3111
+ }
3112
+ const api = new OneApi(apiKey);
3113
+ const spinner5 = createSpinner();
3114
+ spinner5.start("Creating relay endpoint...");
3115
+ try {
3116
+ const body = {
3117
+ connectionKey: options.connectionKey
3118
+ };
3119
+ if (options.description) body.description = options.description;
3120
+ if (options.eventFilters) body.eventFilters = parseJsonArg2(options.eventFilters, "--event-filters");
3121
+ if (options.tags) body.tags = parseJsonArg2(options.tags, "--tags");
3122
+ if (options.createWebhook) body.createWebhook = true;
3123
+ const result = await api.createRelayEndpoint(body);
3124
+ if (isAgentMode()) {
3125
+ json(result);
3126
+ return;
3127
+ }
3128
+ spinner5.stop("Relay endpoint created");
3129
+ console.log();
3130
+ console.log(` ${pc8.dim("ID:")} ${result.id}`);
3131
+ console.log(` ${pc8.dim("URL:")} ${result.url}`);
3132
+ console.log(` ${pc8.dim("Active:")} ${result.active}`);
3133
+ if (result.description) console.log(` ${pc8.dim("Description:")} ${result.description}`);
3134
+ if (result.eventFilters?.length) console.log(` ${pc8.dim("Events:")} ${result.eventFilters.join(", ")}`);
3135
+ if (result.webhookPayload?.id) console.log(` ${pc8.dim("Webhook ID:")} ${result.webhookPayload.id}`);
3136
+ console.log();
3137
+ } catch (error2) {
3138
+ spinner5.stop("Failed to create relay endpoint");
3139
+ error(`Error: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
3140
+ }
3141
+ }
3142
+ async function relayListCommand(options) {
3143
+ const { apiKey } = getConfig3();
3144
+ const api = new OneApi(apiKey);
3145
+ const spinner5 = createSpinner();
3146
+ spinner5.start("Loading relay endpoints...");
3147
+ try {
3148
+ const query = {};
3149
+ if (options.limit) query.limit = options.limit;
3150
+ if (options.page) query.page = options.page;
3151
+ const result = await api.listRelayEndpoints(query);
3152
+ const endpoints = result.rows || [];
3153
+ if (isAgentMode()) {
3154
+ json({
3155
+ total: result.total,
3156
+ showing: endpoints.length,
3157
+ endpoints: endpoints.map((e) => ({
3158
+ id: e.id,
3159
+ active: e.active,
3160
+ description: e.description,
3161
+ eventFilters: e.eventFilters,
3162
+ actionsCount: e.actions?.length || 0,
3163
+ url: e.url,
3164
+ createdAt: e.createdAt
3165
+ }))
3166
+ });
3167
+ return;
3168
+ }
3169
+ spinner5.stop(`${endpoints.length} relay endpoint${endpoints.length === 1 ? "" : "s"} found`);
3170
+ if (endpoints.length === 0) {
3171
+ console.log("\n No relay endpoints yet.\n");
3172
+ return;
3173
+ }
3174
+ printTable(
3175
+ ["Status", "Description", "Events", "Actions", "ID"],
3176
+ endpoints.map((e) => [
3177
+ e.active ? pc8.green("\u25CF") : pc8.dim("\u25CB"),
3178
+ e.description || pc8.dim("(none)"),
3179
+ e.eventFilters?.join(", ") || pc8.dim("all"),
3180
+ String(e.actions?.length || 0),
3181
+ pc8.dim(e.id.slice(0, 8))
3182
+ ])
3183
+ );
3184
+ } catch (error2) {
3185
+ spinner5.stop("Failed to list relay endpoints");
3186
+ error(`Error: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
3187
+ }
3188
+ }
3189
+ async function relayGetCommand(id) {
3190
+ const { apiKey } = getConfig3();
3191
+ const api = new OneApi(apiKey);
3192
+ const spinner5 = createSpinner();
3193
+ spinner5.start("Loading relay endpoint...");
3194
+ try {
3195
+ const result = await api.getRelayEndpoint(id);
3196
+ if (isAgentMode()) {
3197
+ json(result);
3198
+ return;
3199
+ }
3200
+ spinner5.stop("Relay endpoint loaded");
3201
+ console.log();
3202
+ console.log(` ${pc8.dim("ID:")} ${result.id}`);
3203
+ console.log(` ${pc8.dim("URL:")} ${result.url}`);
3204
+ console.log(` ${pc8.dim("Active:")} ${result.active}`);
3205
+ if (result.description) console.log(` ${pc8.dim("Description:")} ${result.description}`);
3206
+ if (result.eventFilters?.length) console.log(` ${pc8.dim("Events:")} ${result.eventFilters.join(", ")}`);
3207
+ console.log(` ${pc8.dim("Actions:")} ${result.actions?.length || 0}`);
3208
+ if (result.actions?.length) {
3209
+ for (const [i, action] of result.actions.entries()) {
3210
+ console.log(` ${pc8.dim(`[${i}]`)} type=${action.type}${action.actionId ? ` actionId=${action.actionId}` : ""}${action.url ? ` url=${action.url}` : ""}`);
3211
+ }
3212
+ }
3213
+ console.log(` ${pc8.dim("Created:")} ${result.createdAt}`);
3214
+ console.log();
3215
+ } catch (error2) {
3216
+ spinner5.stop("Failed to load relay endpoint");
3217
+ error(`Error: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
3218
+ }
3219
+ }
3220
+ async function relayUpdateCommand(id, options) {
3221
+ const { apiKey } = getConfig3();
3222
+ const api = new OneApi(apiKey);
3223
+ const spinner5 = createSpinner();
3224
+ spinner5.start("Updating relay endpoint...");
3225
+ try {
3226
+ const body = {};
3227
+ if (options.description !== void 0) body.description = options.description;
3228
+ if (options.active !== void 0) body.active = options.active;
3229
+ if (options.eventFilters) body.eventFilters = parseJsonArg2(options.eventFilters, "--event-filters");
3230
+ if (options.tags) body.tags = parseJsonArg2(options.tags, "--tags");
3231
+ if (options.actions) body.actions = parseJsonArg2(options.actions, "--actions");
3232
+ const result = await api.updateRelayEndpoint(id, body);
3233
+ if (isAgentMode()) {
3234
+ json(result);
3235
+ return;
3236
+ }
3237
+ spinner5.stop("Relay endpoint updated");
3238
+ console.log(` ${pc8.dim("ID:")} ${result.id}`);
3239
+ console.log(` ${pc8.dim("Active:")} ${result.active}`);
3240
+ console.log(` ${pc8.dim("Actions:")} ${result.actions?.length || 0}`);
3241
+ console.log();
3242
+ } catch (error2) {
3243
+ spinner5.stop("Failed to update relay endpoint");
3244
+ error(`Error: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
3245
+ }
3246
+ }
3247
+ async function relayDeleteCommand(id) {
3248
+ const { apiKey } = getConfig3();
3249
+ const api = new OneApi(apiKey);
3250
+ const spinner5 = createSpinner();
3251
+ spinner5.start("Deleting relay endpoint...");
3252
+ try {
3253
+ const result = await api.deleteRelayEndpoint(id);
3254
+ if (isAgentMode()) {
3255
+ json({ deleted: true, id: result.id });
3256
+ return;
3257
+ }
3258
+ spinner5.stop("Relay endpoint deleted");
3259
+ console.log(` Deleted: ${result.id}`);
3260
+ console.log();
3261
+ } catch (error2) {
3262
+ spinner5.stop("Failed to delete relay endpoint");
3263
+ error(`Error: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
3264
+ }
3265
+ }
3266
+ async function relayActivateCommand(id, options) {
3267
+ const { apiKey } = getConfig3();
3268
+ const api = new OneApi(apiKey);
3269
+ const spinner5 = createSpinner();
3270
+ spinner5.start("Activating relay endpoint...");
3271
+ try {
3272
+ const actions2 = parseJsonArg2(options.actions, "--actions");
3273
+ const body = { actions: actions2 };
3274
+ if (options.webhookSecret) body.webhookSecret = options.webhookSecret;
3275
+ const result = await api.activateRelayEndpoint(id, body);
3276
+ if (isAgentMode()) {
3277
+ json(result);
3278
+ return;
3279
+ }
3280
+ spinner5.stop("Relay endpoint activated");
3281
+ console.log(` ${pc8.dim("ID:")} ${result.id}`);
3282
+ console.log(` ${pc8.dim("Active:")} ${result.active}`);
3283
+ console.log(` ${pc8.dim("Actions:")} ${result.actions?.length || 0}`);
3284
+ console.log();
3285
+ } catch (error2) {
3286
+ spinner5.stop("Failed to activate relay endpoint");
3287
+ error(`Error: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
3288
+ }
3289
+ }
3290
+ async function relayEventsCommand(options) {
3291
+ const { apiKey } = getConfig3();
3292
+ const api = new OneApi(apiKey);
3293
+ const spinner5 = createSpinner();
3294
+ spinner5.start("Loading relay events...");
3295
+ try {
3296
+ const query = {};
3297
+ if (options.limit) query.limit = options.limit;
3298
+ if (options.page) query.page = options.page;
3299
+ if (options.platform) query.platform = options.platform;
3300
+ if (options.eventType) query.eventType = options.eventType;
3301
+ if (options.after) query.after = options.after;
3302
+ if (options.before) query.before = options.before;
3303
+ const result = await api.listRelayEvents(query);
3304
+ const events = result.rows || [];
3305
+ if (isAgentMode()) {
3306
+ json({
3307
+ total: result.total,
3308
+ showing: events.length,
3309
+ events: events.map((e) => ({
3310
+ id: e.id,
3311
+ platform: e.platform,
3312
+ eventType: e.eventType,
3313
+ timestamp: e.timestamp || e.createdAt
3314
+ }))
3315
+ });
3316
+ return;
3317
+ }
3318
+ spinner5.stop(`${events.length} event${events.length === 1 ? "" : "s"} found`);
3319
+ if (events.length === 0) {
3320
+ console.log("\n No events found.\n");
3321
+ return;
3322
+ }
3323
+ printTable(
3324
+ ["Platform", "Event Type", "Timestamp", "ID"],
3325
+ events.map((e) => [
3326
+ e.platform,
3327
+ e.eventType || pc8.dim("unknown"),
3328
+ e.timestamp || e.createdAt,
3329
+ pc8.dim(e.id.slice(0, 8))
3330
+ ])
3331
+ );
3332
+ } catch (error2) {
3333
+ spinner5.stop("Failed to list relay events");
3334
+ error(`Error: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
3335
+ }
3336
+ }
3337
+ async function relayEventGetCommand(id) {
3338
+ const { apiKey } = getConfig3();
3339
+ const api = new OneApi(apiKey);
3340
+ const spinner5 = createSpinner();
3341
+ spinner5.start("Loading relay event...");
3342
+ try {
3343
+ const result = await api.getRelayEvent(id);
3344
+ if (isAgentMode()) {
3345
+ json(result);
3346
+ return;
3347
+ }
3348
+ spinner5.stop("Relay event loaded");
3349
+ console.log();
3350
+ console.log(` ${pc8.dim("ID:")} ${result.id}`);
3351
+ console.log(` ${pc8.dim("Platform:")} ${result.platform}`);
3352
+ console.log(` ${pc8.dim("Event:")} ${result.eventType}`);
3353
+ console.log(` ${pc8.dim("Timestamp:")} ${result.timestamp || result.createdAt}`);
3354
+ console.log(` ${pc8.dim("Payload:")}`);
3355
+ console.log(JSON.stringify(result.payload, null, 2));
3356
+ console.log();
3357
+ } catch (error2) {
3358
+ spinner5.stop("Failed to load relay event");
3359
+ error(`Error: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
3360
+ }
3361
+ }
3362
+ async function relayDeliveriesCommand(options) {
3363
+ if (!options.endpointId && !options.eventId) {
3364
+ error("Provide either --endpoint-id or --event-id");
3365
+ }
3366
+ const { apiKey } = getConfig3();
3367
+ const api = new OneApi(apiKey);
3368
+ const spinner5 = createSpinner();
3369
+ spinner5.start("Loading deliveries...");
3370
+ try {
3371
+ const deliveries = options.endpointId ? await api.listRelayEndpointDeliveries(options.endpointId) : await api.listRelayEventDeliveries(options.eventId);
3372
+ const items = Array.isArray(deliveries) ? deliveries : deliveries.rows || [];
3373
+ if (isAgentMode()) {
3374
+ json({ deliveries: items });
3375
+ return;
3376
+ }
3377
+ spinner5.stop(`${items.length} deliver${items.length === 1 ? "y" : "ies"} found`);
3378
+ if (items.length === 0) {
3379
+ console.log("\n No deliveries found.\n");
3380
+ return;
3381
+ }
3382
+ printTable(
3383
+ ["Status", "Code", "Attempt", "Delivered At", "Error"],
3384
+ items.map((d) => [
3385
+ d.status === "success" ? pc8.green(d.status) : pc8.red(d.status),
3386
+ d.statusCode != null ? String(d.statusCode) : pc8.dim("-"),
3387
+ String(d.attempt),
3388
+ d.deliveredAt || pc8.dim("-"),
3389
+ d.error ? pc8.red(d.error.slice(0, 50)) : pc8.dim("-")
3390
+ ])
3391
+ );
3392
+ } catch (error2) {
3393
+ spinner5.stop("Failed to load deliveries");
3394
+ error(`Error: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
3395
+ }
3396
+ }
3397
+ async function relayEventTypesCommand(platform) {
3398
+ const { apiKey } = getConfig3();
3399
+ const api = new OneApi(apiKey);
3400
+ const spinner5 = createSpinner();
3401
+ spinner5.start(`Loading event types for ${pc8.cyan(platform)}...`);
3402
+ try {
3403
+ const eventTypes = await api.listRelayEventTypes(platform);
3404
+ if (isAgentMode()) {
3405
+ json({ platform, eventTypes });
3406
+ return;
2765
3407
  }
2766
- },
2767
- "steps": [
2768
- {
2769
- "id": "stepId",
2770
- "name": "Human-readable label",
2771
- "type": "action",
2772
- "action": {
2773
- "platform": "stripe",
2774
- "actionId": "the-action-id-from-search",
2775
- "connectionKey": "$.input.stripeConnectionKey",
2776
- "data": {},
2777
- "pathVars": {},
2778
- "queryParams": {},
2779
- "headers": {}
2780
- }
3408
+ spinner5.stop(`${eventTypes.length} event type${eventTypes.length === 1 ? "" : "s"} found`);
3409
+ if (eventTypes.length === 0) {
3410
+ console.log(`
3411
+ No event types found for ${platform}.
3412
+ `);
3413
+ return;
2781
3414
  }
2782
- ]
3415
+ console.log();
3416
+ for (const type of eventTypes) {
3417
+ console.log(` ${type}`);
3418
+ }
3419
+ console.log();
3420
+ } catch (error2) {
3421
+ spinner5.stop("Failed to load event types");
3422
+ error(`Error: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
3423
+ }
2783
3424
  }
2784
- \`\`\`
2785
-
2786
- ### Input declarations
2787
3425
 
2788
- | Field | Type | Description |
2789
- |---|---|---|
2790
- | \`type\` | string | \`string\`, \`number\`, \`boolean\`, \`object\`, \`array\` |
2791
- | \`required\` | boolean | Whether this input must be provided (default: true) |
2792
- | \`default\` | any | Default value if not provided |
2793
- | \`description\` | string | Human-readable description |
2794
- | \`connection\` | object | Connection metadata: \`{ "platform": "gmail" }\` \u2014 enables auto-resolution |
2795
-
2796
- **Connection inputs** have a \`connection\` field. If the user has exactly one connection for that platform, the workflow engine auto-resolves it.
3426
+ // src/commands/guide.ts
3427
+ import pc9 from "picocolors";
2797
3428
 
2798
- ## 4. Selector Syntax Reference
3429
+ // src/lib/guide-content.ts
3430
+ var GUIDE_OVERVIEW = `# One CLI \u2014 Agent Guide
2799
3431
 
2800
- | Pattern | Resolves To |
2801
- |---|---|
2802
- | \`$.input.gmailConnectionKey\` | Input value (including connection keys) |
2803
- | \`$.input.customerEmail\` | Any input parameter |
2804
- | \`$.steps.stepId.response\` | Full API response from a step |
2805
- | \`$.steps.stepId.response.data[0].email\` | Nested field with array index |
2806
- | \`$.steps.stepId.response.data[*].id\` | Wildcard \u2014 maps array to field |
2807
- | \`$.env.MY_VAR\` | Environment variable |
2808
- | \`$.loop.item\` | Current loop item |
2809
- | \`$.loop.i\` | Current loop index |
2810
- | \`"Hello {{$.steps.getUser.response.data.name}}"\` | String interpolation |
3432
+ ## Setup
2811
3433
 
2812
- **Rules:**
2813
- - A value that is purely \`$.xxx\` resolves to the raw type (object, array, number)
2814
- - A string containing \`{{$.xxx}}\` does string interpolation (stringifies objects)
2815
- - Selectors inside objects/arrays are resolved recursively
3434
+ 1. Run \`one init\` to configure your API key
3435
+ 2. Run \`one add <platform>\` to connect platforms via OAuth
3436
+ 3. Run \`one --agent connection list\` to verify connections
2816
3437
 
2817
- ## 5. Step Types Reference
3438
+ ## The --agent Flag
2818
3439
 
2819
- ### \`action\` \u2014 Execute a One API action
3440
+ Always use \`--agent\` for machine-readable JSON output. It disables colors, spinners, and interactive prompts.
2820
3441
 
2821
- \`\`\`json
2822
- {
2823
- "id": "findCustomer",
2824
- "name": "Search Stripe customers",
2825
- "type": "action",
2826
- "action": {
2827
- "platform": "stripe",
2828
- "actionId": "conn_mod_def::xxx::yyy",
2829
- "connectionKey": "$.input.stripeConnectionKey",
2830
- "data": {
2831
- "query": "email:'{{$.input.customerEmail}}'"
2832
- }
2833
- }
2834
- }
3442
+ \`\`\`bash
3443
+ one --agent <command>
2835
3444
  \`\`\`
2836
3445
 
2837
- ### \`transform\` \u2014 Transform data with a JS expression
3446
+ All commands return JSON. If an \`error\` key is present, the command failed.
2838
3447
 
2839
- \`\`\`json
2840
- {
2841
- "id": "extractNames",
2842
- "name": "Extract customer names",
2843
- "type": "transform",
2844
- "transform": {
2845
- "expression": "$.steps.findCustomer.response.data.map(c => c.name)"
2846
- }
2847
- }
2848
- \`\`\`
3448
+ ## IMPORTANT: Read the guide before you act
2849
3449
 
2850
- The expression is evaluated with the full flow context as \`$\`.
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.
2851
3451
 
2852
- ### \`code\` \u2014 Run multi-line JavaScript
3452
+ ## Features
2853
3453
 
2854
- Unlike \`transform\` (single expression, implicit return), \`code\` runs a full function body with explicit \`return\`. Use it when you need variables, loops, try/catch, or \`await\`.
3454
+ ### 1. Actions \u2014 Execute API calls on 200+ platforms
3455
+ Search for actions, read their docs, and execute them. This is the core workflow.
2855
3456
 
2856
- \`\`\`json
2857
- {
2858
- "id": "processData",
2859
- "name": "Process and enrich data",
2860
- "type": "code",
2861
- "code": {
2862
- "source": "const customers = $.steps.listCustomers.response.data;\\nconst enriched = customers.map(c => ({\\n ...c,\\n tier: c.spend > 1000 ? 'gold' : 'silver'\\n}));\\nreturn enriched;"
2863
- }
2864
- }
3457
+ **Quick start:**
3458
+ \`\`\`bash
3459
+ one --agent connection list # See connected platforms
3460
+ one --agent actions search <platform> "<query>" -t execute # Find an action
3461
+ one --agent actions knowledge <platform> <actionId> # Read docs (REQUIRED)
3462
+ one --agent actions execute <platform> <actionId> <key> -d '{}' # Execute it
2865
3463
  \`\`\`
2866
3464
 
2867
- The \`source\` field contains a JS function body. The flow context is available as \`$\`. The function is async, so you can use \`await\`. The return value is stored as the step result.
3465
+ **Parameter flags:**
3466
+ - \`-d <json>\` \u2014 Request body (POST/PUT/PATCH)
3467
+ - \`--path-vars <json>\` \u2014 URL path variables (e.g., \`{"userId": "me"}\`)
3468
+ - \`--query-params <json>\` \u2014 Query parameters (arrays expand to repeated params)
3469
+ - \`--form-url-encoded\` \u2014 Send as form data instead of JSON
3470
+ - \`--dry-run\` \u2014 Preview request without executing
2868
3471
 
2869
- ### \`condition\` \u2014 If/then/else branching
2870
-
2871
- \`\`\`json
2872
- {
2873
- "id": "checkFound",
2874
- "name": "Check if customer was found",
2875
- "type": "condition",
2876
- "condition": {
2877
- "expression": "$.steps.findCustomer.response.data.length > 0",
2878
- "then": [
2879
- { "id": "sendEmail", "name": "Send welcome email", "type": "action", "action": { "..." : "..." } }
2880
- ],
2881
- "else": [
2882
- { "id": "logNotFound", "name": "Log not found", "type": "transform", "transform": { "expression": "'Customer not found'" } }
2883
- ]
2884
- }
2885
- }
2886
- \`\`\`
3472
+ Do NOT pass path or query parameters in \`-d\` \u2014 use the correct flags.
2887
3473
 
2888
- ### \`loop\` \u2014 Iterate over an array
3474
+ ### 2. Flows \u2014 Multi-step API workflows
3475
+ Chain actions across platforms as JSON workflow files with conditions, loops, parallel execution, transforms, and AI analysis via bash steps.
2889
3476
 
2890
- \`\`\`json
2891
- {
2892
- "id": "processOrders",
2893
- "name": "Process each order",
2894
- "type": "loop",
2895
- "loop": {
2896
- "over": "$.steps.listOrders.response.data",
2897
- "as": "order",
2898
- "indexAs": "i",
2899
- "maxIterations": 1000,
2900
- "maxConcurrency": 5,
2901
- "steps": [
2902
- {
2903
- "id": "createInvoice",
2904
- "name": "Create invoice for order",
2905
- "type": "action",
2906
- "action": {
2907
- "platform": "quickbooks",
2908
- "actionId": "...",
2909
- "connectionKey": "$.input.qbConnectionKey",
2910
- "data": { "amount": "$.loop.order.total" }
2911
- }
2912
- }
2913
- ]
2914
- }
2915
- }
3477
+ **Quick start:**
3478
+ \`\`\`bash
3479
+ one --agent flow create <key> --definition '<json>' # Create a workflow
3480
+ one --agent flow validate <key> # Validate it
3481
+ one --agent flow execute <key> -i param=value # Execute it
3482
+ one --agent flow list # List all workflows
2916
3483
  \`\`\`
2917
3484
 
2918
- - \`maxConcurrency\` (optional): When set > 1, loop iterations run in parallel batches of that size. Default is sequential (1).
3485
+ **Key concepts:**
3486
+ - Workflows are JSON files at \`.one/flows/<key>.flow.json\`
3487
+ - 12 step types: action, transform, code, condition, loop, parallel, file-read, file-write, while, flow, paginate, bash
3488
+ - Data wiring via selectors: \`$.input.param\`, \`$.steps.stepId.response\`, \`$.loop.item\`
3489
+ - AI analysis via bash steps: \`claude --print\` with \`parseJson: true\`
3490
+ - Use \`--allow-bash\` to enable bash steps, \`--mock\` for dry-run with mock responses
2919
3491
 
2920
- ### \`parallel\` \u2014 Run steps concurrently
3492
+ ### 3. Relay \u2014 Webhook event forwarding between platforms
3493
+ Receive webhooks from platforms (Stripe, GitHub, Airtable, Attio, Google Calendar) and forward event data to any connected platform using passthrough actions with Handlebars templates. No middleware, no code.
2921
3494
 
2922
- \`\`\`json
2923
- {
2924
- "id": "parallelLookups",
2925
- "name": "Look up in parallel",
2926
- "type": "parallel",
2927
- "parallel": {
2928
- "maxConcurrency": 5,
2929
- "steps": [
2930
- { "id": "getStripe", "name": "Get Stripe data", "type": "action", "action": { "...": "..." } },
2931
- { "id": "getHubspot", "name": "Get HubSpot data", "type": "action", "action": { "...": "..." } }
2932
- ]
2933
- }
2934
- }
3495
+ **Quick start:**
3496
+ \`\`\`bash
3497
+ one --agent relay event-types <platform> # See available events
3498
+ one --agent relay create --connection-key <key> --create-webhook --event-filters '["event.type"]'
3499
+ one --agent relay activate <id> --actions '[{"type":"passthrough","actionId":"...","connectionKey":"...","body":{...}}]'
3500
+ one --agent relay list # List endpoints
3501
+ one --agent relay events --platform <p> # List received events
3502
+ one --agent relay deliveries --endpoint-id <id> # Check delivery status
2935
3503
  \`\`\`
2936
3504
 
2937
- ### \`file-read\` \u2014 Read from filesystem
3505
+ **Key concepts:**
3506
+ - \`passthrough\` actions map webhook fields to another platform's API using Handlebars: \`{{payload.data.object.email}}\`
3507
+ - Template context: \`{{relayEventId}}\`, \`{{platform}}\`, \`{{eventType}}\`, \`{{payload}}\`, \`{{timestamp}}\`, \`{{connectionId}}\`
3508
+ - \`--create-webhook\` auto-registers the webhook URL with the source platform
3509
+ - Use \`actions knowledge\` to learn both the incoming payload shape AND the destination API shape before building templates
2938
3510
 
2939
- \`\`\`json
2940
- {
2941
- "id": "readConfig",
2942
- "name": "Read config file",
2943
- "type": "file-read",
2944
- "fileRead": { "path": "./data/config.json", "parseJson": true }
2945
- }
2946
- \`\`\`
3511
+ ## Topics
2947
3512
 
2948
- ### \`file-write\` \u2014 Write to filesystem
3513
+ Request specific sections:
3514
+ - \`one guide overview\` \u2014 This section
3515
+ - \`one guide actions\` \u2014 Actions reference (search, knowledge, execute)
3516
+ - \`one guide flows\` \u2014 Workflow engine reference (step types, selectors, examples)
3517
+ - \`one guide relay\` \u2014 Webhook relay reference (templates, passthrough actions)
3518
+ - \`one guide all\` \u2014 Everything
2949
3519
 
2950
- \`\`\`json
2951
- {
2952
- "id": "writeResults",
2953
- "name": "Save results",
2954
- "type": "file-write",
2955
- "fileWrite": {
2956
- "path": "./output/results.json",
2957
- "content": "$.steps.transform.output",
2958
- "append": false
2959
- }
2960
- }
2961
- \`\`\`
3520
+ ## Important Notes
2962
3521
 
2963
- ### \`while\` \u2014 Condition-driven loop (do-while)
3522
+ - **Always use \`--agent\` flag** for structured JSON output
3523
+ - Platform names are always **kebab-case** (e.g., \`hub-spot\`, \`google-calendar\`)
3524
+ - Always use the **exact action ID** from search results \u2014 don't guess
3525
+ - Always read **knowledge** before executing any action
3526
+ - Connection keys come from \`one connection list\` \u2014 don't hardcode them
3527
+ `;
3528
+ var GUIDE_ACTIONS = `# One Actions \u2014 Reference
2964
3529
 
2965
- Iterates until a condition becomes falsy. The first iteration always runs (do-while semantics), then the condition is checked before each subsequent iteration. Useful for pagination.
3530
+ ## Workflow: search \u2192 knowledge \u2192 execute
2966
3531
 
2967
- \`\`\`json
2968
- {
2969
- "id": "paginate",
2970
- "name": "Paginate through all pages",
2971
- "type": "while",
2972
- "while": {
2973
- "condition": "$.steps.paginate.output.lastResult.nextPageToken != null",
2974
- "maxIterations": 50,
2975
- "steps": [
2976
- {
2977
- "id": "fetchPage",
2978
- "name": "Fetch next page",
2979
- "type": "action",
2980
- "action": {
2981
- "platform": "gmail",
2982
- "actionId": "GMAIL_LIST_MESSAGES_ACTION_ID",
2983
- "connectionKey": "$.input.gmailKey",
2984
- "queryParams": {
2985
- "pageToken": "$.steps.paginate.output.lastResult.nextPageToken"
2986
- }
2987
- }
2988
- }
2989
- ]
2990
- }
2991
- }
2992
- \`\`\`
3532
+ Always follow this sequence. Never skip the knowledge step.
2993
3533
 
2994
- | Field | Type | Description |
2995
- |---|---|---|
2996
- | \`condition\` | string | JS expression evaluated before each iteration (after iteration 0) |
2997
- | \`maxIterations\` | number | Safety cap, default: 100 |
2998
- | \`steps\` | FlowStep[] | Steps to execute each iteration |
3534
+ ### 1. List Connections
2999
3535
 
3000
- The step output contains \`lastResult\` (last step's output from most recent iteration), \`iteration\` (count), and \`results\` (array of all iteration outputs). Reference via \`$.steps.<id>.output.lastResult\`.
3536
+ \`\`\`bash
3537
+ one --agent connection list
3538
+ \`\`\`
3001
3539
 
3002
- ### \`flow\` \u2014 Execute a sub-flow
3540
+ Returns platforms, status, connection keys, and tags.
3003
3541
 
3004
- Loads and executes another saved flow, enabling flow composition. Circular flows are detected and blocked.
3542
+ ### 2. Search Actions
3005
3543
 
3006
- \`\`\`json
3007
- {
3008
- "id": "processCustomer",
3009
- "name": "Run customer enrichment flow",
3010
- "type": "flow",
3011
- "flow": {
3012
- "key": "enrich-customer",
3013
- "inputs": {
3014
- "email": "$.steps.getCustomer.response.email",
3015
- "connectionKey": "$.input.hubspotConnectionKey"
3016
- }
3017
- }
3018
- }
3544
+ \`\`\`bash
3545
+ one --agent actions search <platform> "<query>" -t execute
3019
3546
  \`\`\`
3020
3547
 
3021
- | Field | Type | Description |
3022
- |---|---|---|
3023
- | \`key\` | string | Flow key or path (supports selectors) |
3024
- | \`inputs\` | object | Input values mapped to the sub-flow's declared inputs (supports selectors) |
3548
+ - Use \`-t execute\` when the user wants to perform an action
3549
+ - Use \`-t knowledge\` (default) for documentation/code generation
3550
+ - Returns up to 5 matching actions with IDs, methods, and paths
3025
3551
 
3026
- ### \`paginate\` \u2014 Auto-collect paginated API results
3552
+ ### 3. Get Knowledge
3027
3553
 
3028
- Automatically pages through a paginated API, collecting all results into a single array.
3029
-
3030
- \`\`\`json
3031
- {
3032
- "id": "allMessages",
3033
- "name": "Fetch all Gmail messages",
3034
- "type": "paginate",
3035
- "paginate": {
3036
- "action": {
3037
- "platform": "gmail",
3038
- "actionId": "GMAIL_LIST_MESSAGES_ACTION_ID",
3039
- "connectionKey": "$.input.gmailKey",
3040
- "queryParams": { "maxResults": 100 }
3041
- },
3042
- "pageTokenField": "nextPageToken",
3043
- "resultsField": "messages",
3044
- "inputTokenParam": "queryParams.pageToken",
3045
- "maxPages": 10
3046
- }
3047
- }
3554
+ \`\`\`bash
3555
+ one --agent actions knowledge <platform> <actionId>
3048
3556
  \`\`\`
3049
3557
 
3050
- | Field | Type | Description |
3051
- |---|---|---|
3052
- | \`action\` | FlowActionConfig | The API action to call (same format as action steps) |
3053
- | \`pageTokenField\` | string | Dot-path in the API response to the next page token |
3054
- | \`resultsField\` | string | Dot-path in the API response to the results array |
3055
- | \`inputTokenParam\` | string | Dot-path in the action config where the page token is injected |
3056
- | \`maxPages\` | number | Maximum pages to fetch, default: 10 |
3558
+ Returns full API docs: required fields, validation rules, request structure. **REQUIRED before execute.** The output includes a CLI PARAMETER MAPPING section showing which flags to use.
3057
3559
 
3058
- ### \`bash\` \u2014 Execute shell commands
3560
+ ### 4. Execute
3059
3561
 
3060
- Runs a shell command. **Requires \`--allow-bash\` flag** for security.
3061
-
3062
- \`\`\`json
3063
- {
3064
- "id": "analyzeData",
3065
- "name": "Analyze data with Claude",
3066
- "type": "bash",
3067
- "bash": {
3068
- "command": "claude --print 'Analyze: {{$.steps.fetchData.response}}' --output-format json",
3069
- "timeout": 120000,
3070
- "parseJson": true
3071
- }
3072
- }
3562
+ \`\`\`bash
3563
+ one --agent actions execute <platform> <actionId> <connectionKey> [options]
3073
3564
  \`\`\`
3074
3565
 
3075
- | Field | Type | Description |
3076
- |---|---|---|
3077
- | \`command\` | string | Shell command to execute (supports selectors and interpolation) |
3078
- | \`timeout\` | number | Timeout in ms, default: 30000 |
3079
- | \`parseJson\` | boolean | Parse stdout as JSON, default: false |
3080
- | \`cwd\` | string | Working directory (supports selectors) |
3081
- | \`env\` | object | Additional environment variables |
3566
+ **Flags:**
3567
+ - \`-d, --data <json>\` \u2014 Request body (POST/PUT/PATCH)
3568
+ - \`--path-vars <json>\` \u2014 Path variables: \`{"userId": "me"}\`
3569
+ - \`--query-params <json>\` \u2014 Query params: \`{"limit": "10"}\`
3570
+ - Arrays expand to repeated params: \`{"metadataHeaders": ["From", "Subject"]}\`
3571
+ - \`--headers <json>\` \u2014 Additional headers
3572
+ - \`--form-data\` / \`--form-url-encoded\` \u2014 Alternative content types
3573
+ - \`--dry-run\` \u2014 Preview without executing
3082
3574
 
3083
- **Security:** Bash steps are blocked by default. Pass \`--allow-bash\` to \`one flow execute\` to enable them.
3575
+ **Do NOT** pass path or query parameters in \`-d\`. Use the correct flags.
3084
3576
 
3085
- ## 6. Error Handling
3577
+ ## Error Handling
3086
3578
 
3087
- ### \`onError\` strategies
3579
+ All errors return JSON: \`{"error": "message"}\`. Check the \`error\` key.
3088
3580
 
3089
- \`\`\`json
3090
- {
3091
- "id": "riskyStep",
3092
- "name": "Might fail",
3093
- "type": "action",
3094
- "onError": {
3095
- "strategy": "retry",
3096
- "retries": 3,
3097
- "retryDelayMs": 1000
3098
- },
3099
- "action": { "...": "..." }
3100
- }
3101
- \`\`\`
3581
+ ## Notes
3102
3582
 
3103
- | Strategy | Behavior |
3104
- |---|---|
3105
- | \`fail\` | Stop the flow immediately (default) |
3106
- | \`continue\` | Mark step as failed, continue to next step |
3107
- | \`retry\` | Retry up to N times with delay |
3108
- | \`fallback\` | On failure, execute a different step |
3583
+ - Platform names are **kebab-case** (e.g., \`hub-spot\`)
3584
+ - JSON flags use single quotes around the JSON to avoid shell escaping
3585
+ - If search returns no results, try broader queries
3586
+ - Access control settings from \`one config\` may restrict execution
3587
+ `;
3588
+ var GUIDE_FLOWS = generateFlowGuide();
3589
+ var GUIDE_RELAY = `# One Relay \u2014 Reference
3109
3590
 
3110
- ### Conditional execution
3591
+ ## Overview
3111
3592
 
3112
- Skip a step based on previous results:
3593
+ Webhook relay receives events from platforms (Stripe, GitHub, Airtable, Attio, Google Calendar) and forwards them to any connected platform using passthrough actions with Handlebars templates.
3113
3594
 
3114
- \`\`\`json
3115
- {
3116
- "id": "sendEmail",
3117
- "name": "Send email only if customer found",
3118
- "type": "action",
3119
- "if": "$.steps.findCustomer.response.data.length > 0",
3120
- "action": { "...": "..." }
3121
- }
3595
+ ## Commands
3596
+
3597
+ \`\`\`bash
3598
+ one --agent relay event-types <platform> # List available events
3599
+ one --agent relay create --connection-key <key> --create-webhook --event-filters '["event.type"]'
3600
+ one --agent relay activate <id> --actions '<json>' # Add forwarding actions
3601
+ one --agent relay list # List endpoints
3602
+ one --agent relay get <id> # Get endpoint details
3603
+ one --agent relay update <id> --actions '<json>' # Update endpoint
3604
+ one --agent relay delete <id> # Delete endpoint
3605
+ one --agent relay events --platform <p> # List received events
3606
+ one --agent relay event <id> # Get event with payload
3607
+ one --agent relay deliveries --endpoint-id <id> # Check delivery status
3122
3608
  \`\`\`
3123
3609
 
3124
- ## 7. Updating Existing Workflows
3610
+ ## Building a Relay
3125
3611
 
3126
- To modify an existing workflow:
3612
+ 1. **Discover connections** \u2014 identify source and destination platforms
3613
+ 2. **Get event types** \u2014 \`one --agent relay event-types <platform>\`
3614
+ 3. **Get source knowledge** \u2014 understand the incoming webhook payload shape (\`{{payload.*}}\` paths)
3615
+ 4. **Get destination knowledge** \u2014 understand the outgoing API body shape
3616
+ 5. **Create endpoint** \u2014 with \`--create-webhook\` and \`--event-filters\`
3617
+ 6. **Activate** \u2014 with passthrough action mapping source fields to destination fields
3127
3618
 
3128
- 1. Read the workflow JSON file at \`.one/flows/<key>.flow.json\`
3129
- 2. Understand its current structure
3130
- 3. Use \`one --agent actions knowledge <platform> <actionId>\` for any new actions
3131
- 4. Modify the JSON (add/remove/update steps, change data mappings, add inputs)
3132
- 5. Write back the updated workflow file
3133
- 6. Validate: \`one --agent flow validate <key>\`
3619
+ ## Template Context
3134
3620
 
3135
- ## 8. Complete Examples
3621
+ | Variable | Description |
3622
+ |----------|-------------|
3623
+ | \`{{relayEventId}}\` | Unique event UUID |
3624
+ | \`{{platform}}\` | Source platform (e.g., \`stripe\`) |
3625
+ | \`{{eventType}}\` | Event type (e.g., \`customer.created\`) |
3626
+ | \`{{payload}}\` | Full incoming webhook body |
3627
+ | \`{{timestamp}}\` | When the event was received |
3628
+ | \`{{connectionId}}\` | Source connection UUID |
3629
+ | \`{{json payload}}\` | Embed full payload as JSON string |
3136
3630
 
3137
- ### Example 1: Simple 2-step workflow \u2014 Search Stripe customer, send Gmail email
3631
+ Access nested fields: \`{{payload.data.object.email}}\`
3138
3632
 
3633
+ ## Action Types
3634
+
3635
+ **passthrough** (primary) \u2014 forward to another platform's API:
3139
3636
  \`\`\`json
3140
3637
  {
3141
- "key": "welcome-customer",
3142
- "name": "Welcome New Customer",
3143
- "description": "Look up a Stripe customer and send them a welcome email",
3144
- "version": "1",
3145
- "inputs": {
3146
- "stripeConnectionKey": {
3147
- "type": "string",
3148
- "required": true,
3149
- "description": "Stripe connection key",
3150
- "connection": { "platform": "stripe" }
3151
- },
3152
- "gmailConnectionKey": {
3153
- "type": "string",
3154
- "required": true,
3155
- "description": "Gmail connection key",
3156
- "connection": { "platform": "gmail" }
3157
- },
3158
- "customerEmail": {
3159
- "type": "string",
3160
- "required": true,
3161
- "description": "Customer email to look up"
3162
- }
3163
- },
3164
- "steps": [
3165
- {
3166
- "id": "findCustomer",
3167
- "name": "Search for customer in Stripe",
3168
- "type": "action",
3169
- "action": {
3170
- "platform": "stripe",
3171
- "actionId": "STRIPE_SEARCH_CUSTOMERS_ACTION_ID",
3172
- "connectionKey": "$.input.stripeConnectionKey",
3173
- "data": {
3174
- "query": "email:'{{$.input.customerEmail}}'"
3175
- }
3176
- }
3177
- },
3178
- {
3179
- "id": "sendWelcome",
3180
- "name": "Send welcome email via Gmail",
3181
- "type": "action",
3182
- "if": "$.steps.findCustomer.response.data && $.steps.findCustomer.response.data.length > 0",
3183
- "action": {
3184
- "platform": "gmail",
3185
- "actionId": "GMAIL_SEND_EMAIL_ACTION_ID",
3186
- "connectionKey": "$.input.gmailConnectionKey",
3187
- "data": {
3188
- "to": "{{$.input.customerEmail}}",
3189
- "subject": "Welcome, {{$.steps.findCustomer.response.data[0].name}}!",
3190
- "body": "Thank you for being a customer. We're glad to have you!"
3191
- }
3192
- }
3193
- }
3194
- ]
3638
+ "type": "passthrough",
3639
+ "actionId": "<action-id>",
3640
+ "connectionKey": "<dest-connection-key>",
3641
+ "body": { "channel": "#alerts", "text": "New customer: {{payload.data.object.name}}" },
3642
+ "eventFilters": ["customer.created"]
3195
3643
  }
3196
3644
  \`\`\`
3197
3645
 
3198
- ### Example 2: Conditional \u2014 Check if HubSpot contact exists, create or update
3199
-
3646
+ **url** \u2014 forward raw event to a URL:
3200
3647
  \`\`\`json
3201
- {
3202
- "key": "sync-hubspot-contact",
3203
- "name": "Sync Contact to HubSpot",
3204
- "description": "Check if a contact exists in HubSpot, create if new or update if existing",
3205
- "version": "1",
3206
- "inputs": {
3207
- "hubspotConnectionKey": {
3208
- "type": "string",
3209
- "required": true,
3210
- "connection": { "platform": "hub-spot" }
3211
- },
3212
- "email": { "type": "string", "required": true },
3213
- "firstName": { "type": "string", "required": true },
3214
- "lastName": { "type": "string", "required": true }
3215
- },
3216
- "steps": [
3217
- {
3218
- "id": "searchContact",
3219
- "name": "Search for existing contact",
3220
- "type": "action",
3221
- "action": {
3222
- "platform": "hub-spot",
3223
- "actionId": "HUBSPOT_SEARCH_CONTACTS_ACTION_ID",
3224
- "connectionKey": "$.input.hubspotConnectionKey",
3225
- "data": {
3226
- "filterGroups": [{ "filters": [{ "propertyName": "email", "operator": "EQ", "value": "$.input.email" }] }]
3227
- }
3228
- }
3229
- },
3230
- {
3231
- "id": "createOrUpdate",
3232
- "name": "Create or update contact",
3233
- "type": "condition",
3234
- "condition": {
3235
- "expression": "$.steps.searchContact.response.total > 0",
3236
- "then": [
3237
- {
3238
- "id": "updateContact",
3239
- "name": "Update existing contact",
3240
- "type": "action",
3241
- "action": {
3242
- "platform": "hub-spot",
3243
- "actionId": "HUBSPOT_UPDATE_CONTACT_ACTION_ID",
3244
- "connectionKey": "$.input.hubspotConnectionKey",
3245
- "pathVars": { "contactId": "$.steps.searchContact.response.results[0].id" },
3246
- "data": {
3247
- "properties": { "firstname": "$.input.firstName", "lastname": "$.input.lastName" }
3248
- }
3249
- }
3250
- }
3251
- ],
3252
- "else": [
3253
- {
3254
- "id": "createContact",
3255
- "name": "Create new contact",
3256
- "type": "action",
3257
- "action": {
3258
- "platform": "hub-spot",
3259
- "actionId": "HUBSPOT_CREATE_CONTACT_ACTION_ID",
3260
- "connectionKey": "$.input.hubspotConnectionKey",
3261
- "data": {
3262
- "properties": { "email": "$.input.email", "firstname": "$.input.firstName", "lastname": "$.input.lastName" }
3263
- }
3264
- }
3265
- }
3266
- ]
3267
- }
3268
- }
3269
- ]
3270
- }
3648
+ {"type": "url", "url": "https://your-app.com/webhook", "eventFilters": ["customer.created"]}
3271
3649
  \`\`\`
3272
3650
 
3273
- ### Example 3: Loop workflow \u2014 Iterate over Shopify orders, create invoices
3274
-
3651
+ **agent** \u2014 send to an agent:
3275
3652
  \`\`\`json
3276
- {
3277
- "key": "shopify-to-invoices",
3278
- "name": "Shopify Orders to Invoices",
3279
- "description": "Fetch recent Shopify orders and create an invoice for each",
3280
- "version": "1",
3281
- "inputs": {
3282
- "shopifyConnectionKey": {
3283
- "type": "string",
3284
- "required": true,
3285
- "connection": { "platform": "shopify" }
3286
- },
3287
- "qbConnectionKey": {
3288
- "type": "string",
3289
- "required": true,
3290
- "connection": { "platform": "quick-books" }
3291
- }
3292
- },
3293
- "steps": [
3294
- {
3295
- "id": "listOrders",
3296
- "name": "List recent Shopify orders",
3297
- "type": "action",
3298
- "action": {
3299
- "platform": "shopify",
3300
- "actionId": "SHOPIFY_LIST_ORDERS_ACTION_ID",
3301
- "connectionKey": "$.input.shopifyConnectionKey",
3302
- "queryParams": { "status": "any", "limit": "50" }
3303
- }
3304
- },
3305
- {
3306
- "id": "createInvoices",
3307
- "name": "Create invoice for each order",
3308
- "type": "loop",
3309
- "loop": {
3310
- "over": "$.steps.listOrders.response.orders",
3311
- "as": "order",
3312
- "indexAs": "i",
3313
- "steps": [
3314
- {
3315
- "id": "createInvoice",
3316
- "name": "Create QuickBooks invoice",
3317
- "type": "action",
3318
- "onError": { "strategy": "continue" },
3319
- "action": {
3320
- "platform": "quick-books",
3321
- "actionId": "QB_CREATE_INVOICE_ACTION_ID",
3322
- "connectionKey": "$.input.qbConnectionKey",
3323
- "data": {
3324
- "Line": [
3325
- {
3326
- "Amount": "$.loop.order.total_price",
3327
- "Description": "Shopify Order #{{$.loop.order.order_number}}"
3328
- }
3329
- ]
3330
- }
3331
- }
3332
- }
3333
- ]
3334
- }
3335
- },
3336
- {
3337
- "id": "summary",
3338
- "name": "Generate summary",
3339
- "type": "transform",
3340
- "transform": {
3341
- "expression": "({ totalOrders: $.steps.listOrders.response.orders.length, processed: $.steps.createInvoices.output.length })"
3342
- }
3343
- }
3344
- ]
3345
- }
3653
+ {"type": "agent", "agentId": "<uuid>", "eventFilters": ["customer.created"]}
3346
3654
  \`\`\`
3347
3655
 
3348
- ## CLI Commands Reference
3349
-
3350
- \`\`\`bash
3351
- # Create a workflow
3352
- one --agent flow create <key> --definition '<json>'
3353
-
3354
- # List all workflows
3355
- one --agent flow list
3356
-
3357
- # Validate a workflow
3358
- one --agent flow validate <key>
3656
+ ## Supported Source Platforms
3359
3657
 
3360
- # Execute a workflow
3361
- one --agent flow execute <key> -i connectionKey=value -i param=value
3658
+ Airtable, Attio, GitHub, Google Calendar, Stripe
3362
3659
 
3363
- # Execute with dry run (validate only)
3364
- one --agent flow execute <key> --dry-run -i connectionKey=value
3660
+ Any connected platform can be a destination via passthrough actions.
3365
3661
 
3366
- # Execute with mock mode (dry-run + mock API responses, runs transforms/code normally)
3367
- one --agent flow execute <key> --dry-run --mock -i connectionKey=value
3662
+ ## Debugging
3368
3663
 
3369
- # Execute with bash steps enabled
3370
- one --agent flow execute <key> --allow-bash -i connectionKey=value
3371
-
3372
- # Execute with verbose output
3373
- one --agent flow execute <key> -v -i connectionKey=value
3374
-
3375
- # List workflow runs
3376
- one --agent flow runs [flowKey]
3377
-
3378
- # Resume a paused/failed run
3379
- one --agent flow resume <runId>
3380
- \`\`\`
3381
-
3382
- ## Important Notes
3383
-
3384
- - **Always use \`--agent\` flag** for structured JSON output
3385
- - **Always call \`one actions knowledge\`** before adding an action step to a workflow
3386
- - Platform names are **kebab-case** (e.g., \`hub-spot\`, not \`HubSpot\`)
3387
- - Connection keys are **inputs**, not hardcoded \u2014 makes workflows portable and shareable
3388
- - Use \`$.input.*\` for input values, \`$.steps.*\` for step results
3389
- - Action IDs in examples (like \`STRIPE_SEARCH_CUSTOMERS_ACTION_ID\`) are placeholders \u2014 always use \`one actions search\` to find the real IDs
3390
- - **Parallel step outputs** are accessible both by index (\`$.steps.parallelStep.output[0]\`) and by substep ID (\`$.steps.substepId.response\`)
3391
- - **Loop step outputs** include iteration details via \`$.steps.myLoop.response.iterations[0].innerStepId.response\`
3392
- - **Code steps** support \`await require('crypto')\`, \`await require('buffer')\`, \`await require('url')\`, \`await require('path')\` \u2014 \`fs\`, \`http\`, \`child_process\`, etc. are blocked
3393
- - **Bash steps** require \`--allow-bash\` flag for security
3394
- - **State is persisted** after every step completion \u2014 resume picks up where it left off
3664
+ 1. \`relay get <id>\` \u2014 verify endpoint is active with actions configured
3665
+ 2. \`relay events --platform <p>\` \u2014 check events are arriving
3666
+ 3. \`relay deliveries --event-id <id>\` \u2014 check delivery status and errors
3667
+ 4. \`relay event <id>\` \u2014 inspect full payload to verify template paths
3395
3668
  `;
3396
3669
  var TOPICS = [
3397
- { topic: "overview", description: "Setup, --agent flag, discovery workflow" },
3670
+ { topic: "overview", description: "Setup, features, and quick start for each" },
3398
3671
  { topic: "actions", description: "Search, read docs, and execute platform actions" },
3399
3672
  { topic: "flows", description: "Build and execute multi-step workflows" },
3673
+ { topic: "relay", description: "Receive webhooks and forward to other platforms" },
3400
3674
  { topic: "all", description: "Complete guide (all topics combined)" }
3401
3675
  ];
3402
3676
  function getGuideContent(topic) {
@@ -3407,10 +3681,12 @@ function getGuideContent(topic) {
3407
3681
  return { title: "One CLI \u2014 Agent Guide: Actions", content: GUIDE_ACTIONS };
3408
3682
  case "flows":
3409
3683
  return { title: "One CLI \u2014 Agent Guide: Workflows", content: GUIDE_FLOWS };
3684
+ case "relay":
3685
+ return { title: "One CLI \u2014 Agent Guide: Relay", content: GUIDE_RELAY };
3410
3686
  case "all":
3411
3687
  return {
3412
3688
  title: "One CLI \u2014 Agent Guide: Complete",
3413
- content: [GUIDE_OVERVIEW, GUIDE_ACTIONS, GUIDE_FLOWS].join("\n---\n\n")
3689
+ content: [GUIDE_OVERVIEW, GUIDE_ACTIONS, GUIDE_FLOWS, GUIDE_RELAY].join("\n---\n\n")
3414
3690
  };
3415
3691
  }
3416
3692
  }
@@ -3419,7 +3695,7 @@ function getAvailableTopics() {
3419
3695
  }
3420
3696
 
3421
3697
  // src/commands/guide.ts
3422
- var VALID_TOPICS = ["overview", "actions", "flows", "all"];
3698
+ var VALID_TOPICS = ["overview", "actions", "flows", "relay", "all"];
3423
3699
  async function guideCommand(topic = "all") {
3424
3700
  if (!VALID_TOPICS.includes(topic)) {
3425
3701
  error(
@@ -3432,14 +3708,14 @@ async function guideCommand(topic = "all") {
3432
3708
  json({ topic, title, content, availableTopics });
3433
3709
  return;
3434
3710
  }
3435
- intro2(pc8.bgCyan(pc8.black(" One Guide ")));
3711
+ intro2(pc9.bgCyan(pc9.black(" One Guide ")));
3436
3712
  console.log();
3437
3713
  console.log(content);
3438
- console.log(pc8.dim("\u2500".repeat(60)));
3714
+ console.log(pc9.dim("\u2500".repeat(60)));
3439
3715
  console.log(
3440
- pc8.dim("Available topics: ") + availableTopics.map((t) => pc8.cyan(t.topic)).join(", ")
3716
+ pc9.dim("Available topics: ") + availableTopics.map((t) => pc9.cyan(t.topic)).join(", ")
3441
3717
  );
3442
- console.log(pc8.dim(`Run ${pc8.cyan("one guide <topic>")} for a specific section.`));
3718
+ console.log(pc9.dim(`Run ${pc9.cyan("one guide <topic>")} for a specific section.`));
3443
3719
  }
3444
3720
 
3445
3721
  // src/lib/platform-meta.ts
@@ -3587,18 +3863,30 @@ The \`--agent\` flag gives structured JSON output. Always include it right
3587
3863
  after \`one\`:
3588
3864
  one --agent <command>
3589
3865
 
3866
+ ### IMPORTANT: Learn before you use
3867
+ Before using any feature (actions, flows, relay), you MUST read the
3868
+ corresponding skill documentation first. The skills are bundled with
3869
+ the CLI and teach you the correct workflow, required steps, and
3870
+ common mistakes to avoid. Never guess \u2014 read the skill, then act.
3871
+
3590
3872
  ### Quick reference:
3591
3873
  - \`one --agent list\` \u2014 See connected platforms and connection keys
3592
3874
  - \`one --agent actions search <platform> "<query>"\` \u2014 Find actions
3593
3875
  - \`one --agent actions knowledge <platform> <actionId>\` \u2014 Read docs (REQUIRED before execute)
3594
3876
  - \`one --agent actions execute <platform> <actionId> <connectionKey>\` \u2014 Execute action
3595
3877
  - \`one --agent flow create\` \u2014 Build multi-step workflows
3878
+ - \`one --agent relay create\` \u2014 Set up webhook relay (receive events, forward to other platforms)
3596
3879
  - \`one --agent guide\` \u2014 Full documentation
3597
3880
  - \`one add <platform>\` \u2014 Connect a new platform (interactive, no --agent)
3598
3881
 
3599
3882
  ### Workflow: search -> knowledge -> execute
3600
3883
  Always read the knowledge before executing. It tells you required parameters,
3601
3884
  validation rules, and platform-specific details.
3885
+
3886
+ ### Webhook Relay
3887
+ Use \`one relay\` to receive webhooks from platforms (Stripe, GitHub, etc.)
3888
+ and forward event data to other platforms using passthrough actions with
3889
+ Handlebars templates. No middleware needed.
3602
3890
  \`\`\``);
3603
3891
  sections.push(buildCurrentState(connections));
3604
3892
  sections.push(`## How To Use the CLI
@@ -3624,6 +3912,17 @@ The \`--agent\` flag goes right after \`one\`, before the subcommand.
3624
3912
  Use \`one flow create\` to build JSON workflows that chain actions across
3625
3913
  platforms with conditions, loops, parallel execution, and transforms.
3626
3914
 
3915
+ ### Webhook Relay:
3916
+ Use \`one relay create\` to receive webhooks from platforms (Stripe, GitHub,
3917
+ Airtable, Attio, Google Calendar) and forward event data to any connected
3918
+ platform using passthrough actions with Handlebars templates.
3919
+
3920
+ ### IMPORTANT: Learn before you use
3921
+ Before using flows or relay, you MUST read the corresponding skill
3922
+ documentation first. The skills teach you the correct workflow, template
3923
+ syntax, required steps, and common mistakes. Never guess \u2014 read the
3924
+ skill, then act.
3925
+
3627
3926
  Run \`one --agent guide\` for the complete reference documentation with examples.`);
3628
3927
  sections.push(`## What to tell the user
3629
3928
 
@@ -3849,6 +4148,14 @@ program.name("one").option("--agent", "Machine-readable JSON output (no colors,
3849
4148
  one flow execute <key> Execute a workflow
3850
4149
  one flow validate <key> Validate a flow
3851
4150
 
4151
+ Webhook Relay:
4152
+ one relay create Create a relay endpoint for a connection
4153
+ one relay list List relay endpoints
4154
+ one relay activate <id> Activate with passthrough actions
4155
+ one relay event-types <platform> List supported event types
4156
+ one relay events List received webhook events
4157
+ one relay deliveries List delivery attempts
4158
+
3852
4159
  Example \u2014 send an email through Gmail:
3853
4160
  $ one list
3854
4161
  # Find: gmail operational live::gmail::default::abc123
@@ -3943,7 +4250,41 @@ flow.command("resume <runId>").description("Resume a paused or failed workflow r
3943
4250
  flow.command("runs [flowKey]").description("List workflow runs (optionally filtered by flow key)").action(async (flowKey) => {
3944
4251
  await flowRunsCommand(flowKey);
3945
4252
  });
3946
- program.command("guide [topic]").description("Full CLI usage guide for agents (topics: overview, actions, flows, all)").action(async (topic) => {
4253
+ flow.command("scaffold [template]").description("Generate a workflow scaffold (templates: basic, conditional, loop, ai)").action(async (template) => {
4254
+ await flowScaffoldCommand(template);
4255
+ });
4256
+ var relay = program.command("relay").alias("r").description("Receive webhooks from platforms and relay them via passthrough actions");
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) => {
4258
+ await relayCreateCommand(options);
4259
+ });
4260
+ relay.command("list").alias("ls").description("List all relay endpoints").option("--limit <n>", "Max results per page").option("--page <n>", "Page number").action(async (options) => {
4261
+ await relayListCommand(options);
4262
+ });
4263
+ relay.command("get <id>").description("Get details of a relay endpoint").action(async (id) => {
4264
+ await relayGetCommand(id);
4265
+ });
4266
+ relay.command("update <id>").description("Update a relay endpoint").option("--description <desc>", "Update description").option("--active", "Set active").option("--no-active", "Set inactive").option("--event-filters <json>", "JSON array of event types").option("--tags <json>", "JSON array of tags").option("--actions <json>", "JSON array of actions (url, passthrough, or agent)").action(async (id, options) => {
4267
+ await relayUpdateCommand(id, options);
4268
+ });
4269
+ relay.command("delete <id>").description("Delete a relay endpoint").action(async (id) => {
4270
+ await relayDeleteCommand(id);
4271
+ });
4272
+ relay.command("activate <id>").description("Activate a relay endpoint with forwarding actions").requiredOption("--actions <json>", "JSON array of actions (url, passthrough, or agent)").option("--webhook-secret <secret>", "Webhook signing secret for signature verification").action(async (id, options) => {
4273
+ await relayActivateCommand(id, options);
4274
+ });
4275
+ relay.command("events").description("List received webhook events").option("--limit <n>", "Max results per page").option("--page <n>", "Page number").option("--platform <platform>", "Filter by platform").option("--event-type <type>", "Filter by event type").option("--after <iso>", "Events after this timestamp").option("--before <iso>", "Events before this timestamp").action(async (options) => {
4276
+ await relayEventsCommand(options);
4277
+ });
4278
+ relay.command("event <id>").description("Get details of a specific webhook event").action(async (id) => {
4279
+ await relayEventGetCommand(id);
4280
+ });
4281
+ relay.command("deliveries").description("List delivery attempts for an endpoint or event").option("--endpoint-id <id>", "Relay endpoint ID").option("--event-id <id>", "Relay event ID").action(async (options) => {
4282
+ await relayDeliveriesCommand(options);
4283
+ });
4284
+ relay.command("event-types <platform>").description("List supported webhook event types for a platform").action(async (platform) => {
4285
+ await relayEventTypesCommand(platform);
4286
+ });
4287
+ program.command("guide [topic]").description("Full CLI usage guide for agents (topics: overview, actions, flows, relay, all)").action(async (topic) => {
3947
4288
  await guideCommand(topic);
3948
4289
  });
3949
4290
  program.command("onboard").description("Agent onboarding \u2014 teaches your agent what the One CLI can do").option("--step <number>", "Run a specific onboarding step (1, 2, or 3)").action(async (options) => {