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