@octaviaflow/flow-rules 0.12.0 → 0.13.1

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
@@ -45,6 +45,7 @@ var ActionType;
45
45
  ActionType["SpeechToText"] = "speech_to_text";
46
46
  ActionType["TextToSpeech"] = "text_to_speech";
47
47
  ActionType["Transform"] = "transform";
48
+ ActionType["Aggregate"] = "aggregate";
48
49
  ActionType["FileOperation"] = "file_operation";
49
50
  ActionType["FileAction"] = "file_action";
50
51
  ActionType["CsvAction"] = "csv_action";
@@ -1373,7 +1374,7 @@ var queryDefinition = {
1373
1374
  key: "visualConfig.limit",
1374
1375
  label: "Limit",
1375
1376
  type: "number" /* Number */,
1376
- description: "Maximum number of rows to return",
1377
+ description: "Rows the PREVIEW shows. A run never reads it — set visualConfig.maxRows to cap a run.",
1377
1378
  defaultValue: 10,
1378
1379
  validation: { required: false, min: 1, max: 1e4 },
1379
1380
  conditional: [
@@ -1385,6 +1386,22 @@ var queryDefinition = {
1385
1386
  }
1386
1387
  ]
1387
1388
  },
1389
+ {
1390
+ key: "visualConfig.maxRows",
1391
+ label: "Max rows (run)",
1392
+ type: "number" /* Number */,
1393
+ description: "The most rows a run reads (up to 10,000). Unset: 10,000. The Limit above is preview-only.",
1394
+ validation: { required: false, min: 1, max: 1e4 },
1395
+ advanced: true,
1396
+ conditional: [
1397
+ {
1398
+ field: "mode",
1399
+ operator: "==",
1400
+ value: "visual" /* Visual */,
1401
+ action: "show"
1402
+ }
1403
+ ]
1404
+ },
1388
1405
  {
1389
1406
  key: "sqlConfig.sql",
1390
1407
  label: "SQL Query",
@@ -1431,7 +1448,7 @@ var queryDefinition = {
1431
1448
  key: "maxRows",
1432
1449
  label: "Max Rows",
1433
1450
  type: "number" /* Number */,
1434
- description: "Maximum number of rows to return",
1451
+ description: "Legacy step-level field — NOT read at run. Cap a visual Query with visualConfig.maxRows; in SQL mode write a LIMIT.",
1435
1452
  defaultValue: 1000,
1436
1453
  validation: { required: false, min: 1, max: 1e4 },
1437
1454
  advanced: true
@@ -1587,6 +1604,46 @@ var transformDefinition = {
1587
1604
  testable: true
1588
1605
  };
1589
1606
 
1607
+ // src/catalog/general/aggregate/index.ts
1608
+ var aggregateDefinition = {
1609
+ id: "aggregate",
1610
+ name: "Aggregate",
1611
+ description: "Total, count or average the upstream records into one row — or one row per group — of computed values " + "(sum, count, avg, min, max, count_distinct, first, last). Put it after the step that produces the rows and " + "before the step that writes the result, for example a TOTAL row under the rows a Transform just wrote.",
1612
+ category: "transform" /* Transform */,
1613
+ type: "aggregate" /* Aggregate */,
1614
+ tags: ["aggregate", "sum", "total", "count", "average", "min", "max", "group by", "rollup", "built-in"],
1615
+ status: "stable" /* Stable */,
1616
+ icon: "Sigma",
1617
+ color: "#0043ce",
1618
+ inputSchema: { fields: [] },
1619
+ outputSchema: {
1620
+ type: "object",
1621
+ properties: [
1622
+ {
1623
+ key: "data",
1624
+ type: "array" /* Array */,
1625
+ description: "One row per group (a single row when there is no group-by): the group keys, then each metric under its `as` key"
1626
+ },
1627
+ {
1628
+ key: "_meta",
1629
+ type: "object" /* Object */,
1630
+ description: "rowsIn, groups, and ignored — values skipped for a numeric metric because they were not numbers"
1631
+ }
1632
+ ],
1633
+ sampleOutput: {
1634
+ data: [{ TotalAmount: 12850.4, Invoices: 35 }],
1635
+ _meta: { rowsIn: 35, groups: 1, ignored: 0 }
1636
+ },
1637
+ dynamic: true
1638
+ },
1639
+ version: "1.0.0",
1640
+ createdAt: "2026-10-08T00:00:00Z",
1641
+ updatedAt: "2026-10-08T00:00:00Z",
1642
+ createdBy: "system",
1643
+ isOfficial: true,
1644
+ testable: false
1645
+ };
1646
+
1590
1647
  // src/catalog/storage/csv-action/index.ts
1591
1648
  var csvActionDefinition = {
1592
1649
  id: "csv_action",
@@ -2508,7 +2565,7 @@ var graphqlQueryDefinition = {
2508
2565
  var aiAgentDefinition = {
2509
2566
  id: "ai_agent",
2510
2567
  name: "AI Agent",
2511
- description: "Run a reusable AI agent. Pick a saved agent from /ai/agents; optionally override model, temperature, or instructions per step.",
2568
+ description: "Run a reusable AI agent. Pick a saved agent from /ai/agents; optionally override model, temperature, or instructions per step. " + "When the step before it produces rows (a Query, a Transform), the agent runs ONCE PER ROW with the prompt as the per-row template — " + "reference single fields, never the whole table; put an Aggregate first to call it once. maxRows (default 50) caps the calls.",
2512
2569
  category: "ai" /* AI */,
2513
2570
  type: "ai_agent" /* AIAgent */,
2514
2571
  tags: ["ai", "agent", "chat", "built-in"],
@@ -2528,21 +2585,28 @@ var aiAgentDefinition = {
2528
2585
  key: "input",
2529
2586
  label: "Input",
2530
2587
  type: "string" /* String */,
2531
- description: "Chat agents: a string (template-supported via {{step.field}}). ETL agents: rows array. When blank, falls back to upstream step output."
2588
+ description: "The prompt. Over a row-shaped upstream it is the per-row template: each data reference resolves to the current row. " + "A reference to the whole upstream table is refused. When blank, the upstream output is the input."
2532
2589
  },
2533
2590
  {
2534
2591
  key: "overrides",
2535
2592
  label: "Overrides",
2536
2593
  type: "json" /* JSON */,
2537
2594
  description: "Optional per-step overrides of the agent’s own settings: model, temperature, maxTokens, topP, instructions and instructionsMode."
2595
+ },
2596
+ {
2597
+ key: "maxRows",
2598
+ label: "Max rows",
2599
+ type: "number" /* Number */,
2600
+ description: "Per-row mode: the most model calls this step may make, one per upstream row (default 50, at most 1000)."
2538
2601
  }
2539
2602
  ]
2540
2603
  },
2541
2604
  outputSchema: {
2542
2605
  type: "object",
2543
2606
  properties: [
2544
- { key: "text", type: "string" /* String */, description: "Agent response text (chat agents)" },
2545
- { key: "rows", type: "json" /* JSON */, description: "Output rows (etl agents)" },
2607
+ { key: "text", type: "string" /* String */, description: "Agent response text; in per-row mode, one line per row" },
2608
+ { key: "output", type: "json" /* JSON */, description: "The parsed answer (an object when the agent replies in JSON); in per-row mode, one entry per row" },
2609
+ { key: "rows", type: "json" /* JSON */, description: "Per-row mode: one entry per upstream row" },
2546
2610
  { key: "usage", type: "json" /* JSON */, description: "Token usage + cost: tokensIn, tokensOut, latencyMs, costUsd" },
2547
2611
  { key: "model", type: "string" /* String */, description: "Model that produced the response" },
2548
2612
  { key: "agentVersion", type: "number" /* Number */, description: "Version of the agent definition that ran" }
@@ -2646,7 +2710,8 @@ var actionsRegistry = {
2646
2710
  console_log: consoleLogDefinition,
2647
2711
  delay: delayDefinition,
2648
2712
  query: queryDefinition,
2649
- transform: transformDefinition
2713
+ transform: transformDefinition,
2714
+ aggregate: aggregateDefinition
2650
2715
  },
2651
2716
  ["logic" /* Logic */]: {
2652
2717
  condition: conditionDefinition,
@@ -3624,6 +3689,9 @@ var EmailConfig = {
3624
3689
  }
3625
3690
  }
3626
3691
  };
3692
+ // src/catalog/isOneOf.ts
3693
+ var isOneOf = (set, value) => typeof value === "string" && set.includes(value);
3694
+
3627
3695
  // src/catalog/general/transform/config.ts
3628
3696
  var TRANSFORM_MODES = ["visual", "expression"];
3629
3697
  var TRANSFORM_OPERATIONS = [
@@ -3633,7 +3701,6 @@ var TRANSFORM_OPERATIONS = [
3633
3701
  { id: "delete", label: "Delete" }
3634
3702
  ];
3635
3703
  var TRANSFORM_OPERATION_IDS = TRANSFORM_OPERATIONS.map((o) => o.id);
3636
- var isOneOf = (set, value) => typeof value === "string" && set.includes(value);
3637
3704
  var DATABASE_CONNECTOR_IDS = new Set(["mongodb", "postgresql", "mysql", "sqlite", "google_sheets"]);
3638
3705
  var TransformConfig = {
3639
3706
  defaults: {
@@ -3735,6 +3802,313 @@ var TransformConfig = {
3735
3802
  return `map_${Date.now()}_${randomSuffix(5)}`;
3736
3803
  }
3737
3804
  };
3805
+ // src/catalog/general/aggregate/config.ts
3806
+ var AGGREGATE_FUNCTIONS = [
3807
+ "sum",
3808
+ "count",
3809
+ "avg",
3810
+ "min",
3811
+ "max",
3812
+ "count_distinct",
3813
+ "first",
3814
+ "last"
3815
+ ];
3816
+ var AGGREGATE_FUNCTION_DESCRIPTIONS = {
3817
+ sum: "add up the numeric values of a field",
3818
+ count: "count the records (with a field: the records where it is not blank)",
3819
+ avg: "the mean of the numeric values of a field",
3820
+ min: "the smallest value of a field",
3821
+ max: "the largest value of a field",
3822
+ count_distinct: "how many different values a field has",
3823
+ first: "the field's value on the first record",
3824
+ last: "the field's value on the last record"
3825
+ };
3826
+ var RESERVED_KEYS2 = new Set(["_meta", "_lineage", "_redacted", "_error"]);
3827
+ var KEY_PATTERN2 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
3828
+ var NUMBER_VALUED = new Set(["sum", "avg", "count", "count_distinct"]);
3829
+ function groupKeyName(path) {
3830
+ const trimmed = String(path ?? "").trim().replace(/\[\]$/, "");
3831
+ const segments = trimmed.split(".").filter(Boolean);
3832
+ return segments.length > 0 ? segments[segments.length - 1] : trimmed;
3833
+ }
3834
+ function schemaAt(schema, path) {
3835
+ let cur = schema;
3836
+ for (const part of path.replace(/\[\]$/, "").split(".").filter(Boolean)) {
3837
+ if (!cur)
3838
+ return;
3839
+ if (cur.type === "array" && cur.items)
3840
+ cur = cur.items;
3841
+ const next = cur.properties?.[part];
3842
+ if (!next)
3843
+ return;
3844
+ cur = next;
3845
+ }
3846
+ return cur;
3847
+ }
3848
+ function leafOf(node, fallback) {
3849
+ const t = node?.type;
3850
+ if (t === "number" || t === "integer" || t === "string" || t === "boolean" || t === "date")
3851
+ return { type: t };
3852
+ return { type: fallback };
3853
+ }
3854
+ function metricSchema(metric, source) {
3855
+ const what = metric.field ? `${metric.fn} of ${metric.field}` : metric.fn;
3856
+ if (NUMBER_VALUED.has(metric.fn))
3857
+ return { type: "number", description: what };
3858
+ return { ...leafOf(metric.field ? schemaAt(source, metric.field) : undefined, "string"), description: what };
3859
+ }
3860
+ var FUNCTION_LIST = AGGREGATE_FUNCTIONS.join(", ");
3861
+ var AggregateConfig = {
3862
+ schema: {
3863
+ arrayPath: {
3864
+ type: "select",
3865
+ required: false,
3866
+ dynamic: true,
3867
+ description: "Which upstream records to aggregate: a path to an array in the upstream step's output. " + "Leave empty for the records the previous step produced (its data). The values come from that step's data, not from the catalog."
3868
+ },
3869
+ groupBy: {
3870
+ type: "array",
3871
+ required: false,
3872
+ description: 'Record fields (dot-paths) to group by, e.g. ["CustomerRef.name"]. Empty: one row for all records. ' + 'Each key lands on the output row under its last segment ("name").'
3873
+ },
3874
+ metrics: {
3875
+ type: "array",
3876
+ required: true,
3877
+ description: 'What to compute, at least one: [{ "fn", "field", "as", "decimals"? }]. ' + `fn is one of ${FUNCTION_LIST} (` + AGGREGATE_FUNCTIONS.map((fn) => `${fn}: ${AGGREGATE_FUNCTION_DESCRIPTIONS[fn]}`).join("; ") + "). field is the record key it reads (a dot-path; count may omit it to count records). " + "as is the output key. decimals rounds a sum or avg. " + 'Example: [{ "fn": "sum", "field": "Balance", "as": "TotalAmount", "decimals": 2 }, { "fn": "count", "as": "Invoices" }]'
3878
+ },
3879
+ outputAs: {
3880
+ type: "string",
3881
+ required: false,
3882
+ description: "The name later steps use for this result, e.g. totals → {{totals[].TotalAmount}} in a Transform mapping. " + "A message or AI prompt reads it as {{ods://steps/<this step id>/output/data[0]/TotalAmount}} — the mapping form is not resolved there. " + "Defaults to the step name."
3883
+ }
3884
+ },
3885
+ defaults: {
3886
+ arrayPath: "",
3887
+ groupBy: [],
3888
+ metrics: [],
3889
+ outputAs: ""
3890
+ },
3891
+ validate(config) {
3892
+ const errors = [];
3893
+ const metrics = Array.isArray(config.metrics) ? config.metrics : [];
3894
+ if (metrics.length === 0) {
3895
+ errors.push('Add at least one metric — what to compute, e.g. { "fn": "sum", "field": "Balance", "as": "TotalAmount" }.');
3896
+ }
3897
+ const groupKeys = [];
3898
+ const groupBy = config.groupBy;
3899
+ if (groupBy !== undefined && groupBy !== null) {
3900
+ if (!Array.isArray(groupBy)) {
3901
+ errors.push('groupBy must be a list of record fields (dot-paths), e.g. ["CustomerRef.name"].');
3902
+ } else {
3903
+ for (const g of groupBy) {
3904
+ if (typeof g !== "string" || g.trim() === "") {
3905
+ errors.push("groupBy has an empty entry — each one is a record field (dot-path).");
3906
+ continue;
3907
+ }
3908
+ groupKeys.push(groupKeyName(g));
3909
+ }
3910
+ }
3911
+ }
3912
+ const seen = new Set;
3913
+ for (const key of groupKeys) {
3914
+ if (seen.has(key)) {
3915
+ errors.push(`Two group-by fields both land under "${key}" — group by fields whose last segment differs.`);
3916
+ }
3917
+ seen.add(key);
3918
+ }
3919
+ metrics.forEach((m, i) => {
3920
+ const where = m && typeof m === "object" && typeof m.as === "string" && m.as ? `"${m.as}"` : `metric ${i + 1}`;
3921
+ if (!m || typeof m !== "object") {
3922
+ errors.push(`${where}: a metric is an object { "fn", "field", "as" }.`);
3923
+ return;
3924
+ }
3925
+ const fn = m.fn;
3926
+ const fnValid = isOneOf(AGGREGATE_FUNCTIONS, fn);
3927
+ if (fn === undefined || fn === null || fn === "") {
3928
+ errors.push(`${where}: fn is required — one of ${FUNCTION_LIST}.`);
3929
+ } else if (!fnValid) {
3930
+ errors.push(`${where}: fn must be one of: ${FUNCTION_LIST} (got "${String(fn)}").`);
3931
+ }
3932
+ const field = m.field;
3933
+ if (field !== undefined && field !== null && typeof field !== "string") {
3934
+ errors.push(`${where}: field must be a string — a dot-path on the record, e.g. "Balance".`);
3935
+ } else if (fnValid && fn !== "count" && (typeof field !== "string" || field.trim() === "")) {
3936
+ errors.push(`${where}: ${fn} needs a field — the record key it reads, e.g. "Balance".`);
3937
+ }
3938
+ const as = m.as;
3939
+ if (typeof as !== "string" || as === "") {
3940
+ errors.push(`${where}: as is required — the output key the value lands under, e.g. "TotalAmount".`);
3941
+ } else if (RESERVED_KEYS2.has(as)) {
3942
+ errors.push(`"${as}" is reserved — pick another output key.`);
3943
+ } else if (!KEY_PATTERN2.test(as)) {
3944
+ errors.push(`"${as}" is not a valid output key (letters, digits, underscore; must not start with a digit).`);
3945
+ } else if (seen.has(as)) {
3946
+ errors.push(`Duplicate output key "${as}" — every metric and group-by key lands under its own name.`);
3947
+ } else {
3948
+ seen.add(as);
3949
+ }
3950
+ const decimals = m.decimals;
3951
+ if (decimals !== undefined && decimals !== null) {
3952
+ if (typeof decimals !== "number" || !Number.isInteger(decimals) || decimals < 0 || decimals > 10) {
3953
+ errors.push(`${where}: decimals must be a whole number from 0 to 10.`);
3954
+ }
3955
+ }
3956
+ });
3957
+ const outputAs = config.outputAs;
3958
+ if (outputAs !== undefined && outputAs !== null && outputAs !== "") {
3959
+ if (typeof outputAs !== "string" || !KEY_PATTERN2.test(outputAs)) {
3960
+ errors.push('outputAs must be a single name (letters, digits, underscore), e.g. "totals" — later steps read {{totals[].<key>}}.');
3961
+ }
3962
+ }
3963
+ const arrayPath = config.arrayPath;
3964
+ if (arrayPath !== undefined && arrayPath !== null && typeof arrayPath !== "string") {
3965
+ errors.push("arrayPath must be a string — a path to an array in the upstream output, or empty for its records.");
3966
+ }
3967
+ return { valid: errors.length === 0, errors };
3968
+ },
3969
+ computeOutputSchema(config, source) {
3970
+ const properties = {};
3971
+ for (const g of Array.isArray(config.groupBy) ? config.groupBy : []) {
3972
+ if (typeof g !== "string" || g.trim() === "")
3973
+ continue;
3974
+ properties[groupKeyName(g)] = { ...leafOf(schemaAt(source, g), "string"), description: `group: ${g}` };
3975
+ }
3976
+ for (const m of Array.isArray(config.metrics) ? config.metrics : []) {
3977
+ if (!m || typeof m !== "object" || typeof m.as !== "string" || !m.as || !isOneOf(AGGREGATE_FUNCTIONS, m.fn))
3978
+ continue;
3979
+ properties[m.as] = metricSchema(m, source);
3980
+ }
3981
+ return { type: "object", properties };
3982
+ },
3983
+ merge(userConfig) {
3984
+ return { ...AggregateConfig.defaults, ...userConfig };
3985
+ },
3986
+ getLabel(config) {
3987
+ const metrics = Array.isArray(config.metrics) ? config.metrics : [];
3988
+ const parts = metrics.filter((m) => m && typeof m === "object" && m.fn).map((m) => m.field ? `${m.fn}(${m.field})` : String(m.fn));
3989
+ const by = Array.isArray(config.groupBy) && config.groupBy.length > 0 ? ` by ${config.groupBy.map(groupKeyName).join(", ")}` : "";
3990
+ return parts.length > 0 ? `${parts.join(", ")}${by}` : "Aggregate";
3991
+ }
3992
+ };
3993
+ // src/catalog/logic/for-each/recordSchema.ts
3994
+ function resolveSchemaPath(root, path) {
3995
+ if (!root)
3996
+ return;
3997
+ const parts = path.split(".").filter(Boolean);
3998
+ let cur = root;
3999
+ for (const part of parts) {
4000
+ if (cur?.type === "object" && cur.properties && part in cur.properties) {
4001
+ cur = cur.properties[part];
4002
+ continue;
4003
+ }
4004
+ if (cur?.type === "array" && cur.items?.properties && part in cur.items.properties) {
4005
+ cur = cur.items.properties[part];
4006
+ continue;
4007
+ }
4008
+ return;
4009
+ }
4010
+ if (cur?.type === "array" && cur.items)
4011
+ return cur.items;
4012
+ if (cur?.type === "object" && cur.properties)
4013
+ return cur;
4014
+ return;
4015
+ }
4016
+ function resolveSamplePath(sample, path) {
4017
+ const parts = path.split(".").filter(Boolean);
4018
+ let cur = sample;
4019
+ for (const part of parts) {
4020
+ if (cur === null || cur === undefined)
4021
+ return;
4022
+ if (Array.isArray(cur)) {
4023
+ if (cur.length === 0 || typeof cur[0] !== "object" || cur[0] === null)
4024
+ return;
4025
+ cur = cur[0][part];
4026
+ continue;
4027
+ }
4028
+ if (typeof cur === "object") {
4029
+ cur = cur[part];
4030
+ continue;
4031
+ }
4032
+ return;
4033
+ }
4034
+ return cur;
4035
+ }
4036
+ function sampleRecords(node) {
4037
+ const output = node?.executionData?.output;
4038
+ if (!output)
4039
+ return null;
4040
+ if (Array.isArray(output))
4041
+ return output;
4042
+ const data = output.data;
4043
+ return Array.isArray(data) ? data : null;
4044
+ }
4045
+ var isRootPath = (path) => path === "" || path === "data";
4046
+ function resolveRecordSchema(upstreamNodes, arrayPath) {
4047
+ const node = upstreamNodes?.[0];
4048
+ if (!node)
4049
+ return;
4050
+ const path = (arrayPath || "").trim();
4051
+ const isRoot = isRootPath(path);
4052
+ const schema = node.outputSchema || node.config?._outputSchema;
4053
+ if (schema?.properties) {
4054
+ if (isRoot) {
4055
+ const hasDeepFields = Object.values(schema.properties).some((f) => f?.properties || f?.items);
4056
+ if (hasDeepFields)
4057
+ return schema;
4058
+ } else {
4059
+ const resolved = resolveSchemaPath(schema, path);
4060
+ if (resolved)
4061
+ return resolved;
4062
+ }
4063
+ }
4064
+ const records = sampleRecords(node);
4065
+ if (records && records.length > 0 && typeof records[0] === "object" && records[0] !== null) {
4066
+ const sample = records[0];
4067
+ if (isRoot)
4068
+ return inferSchemaFromSample(sample);
4069
+ const resolved = resolveSamplePath(sample, path);
4070
+ if (Array.isArray(resolved) && resolved.length > 0 && typeof resolved[0] === "object" && resolved[0] !== null) {
4071
+ return inferSchemaFromSample(resolved[0]);
4072
+ }
4073
+ if (resolved && typeof resolved === "object" && !Array.isArray(resolved)) {
4074
+ return inferSchemaFromSample(resolved);
4075
+ }
4076
+ if (!path.includes("."))
4077
+ return inferSchemaFromSample(sample);
4078
+ }
4079
+ return schema?.properties ? schema : undefined;
4080
+ }
4081
+ function detectRecordFields(upstreamNodes, arrayPath) {
4082
+ const set = new Set;
4083
+ const props = resolveRecordSchema(upstreamNodes, arrayPath)?.properties;
4084
+ if (props && typeof props === "object")
4085
+ for (const k of Object.keys(props))
4086
+ set.add(k);
4087
+ const records = sampleRecords(upstreamNodes?.[0]);
4088
+ if (records) {
4089
+ const path = (arrayPath || "").trim();
4090
+ const addKeys = (v) => {
4091
+ if (v && typeof v === "object" && !Array.isArray(v))
4092
+ for (const k of Object.keys(v))
4093
+ set.add(k);
4094
+ };
4095
+ for (const r of records.slice(0, 25)) {
4096
+ if (isRootPath(path)) {
4097
+ addKeys(r);
4098
+ continue;
4099
+ }
4100
+ const at = resolveSamplePath(r, path);
4101
+ if (Array.isArray(at))
4102
+ for (const item of at.slice(0, 25))
4103
+ addKeys(item);
4104
+ else if (at && typeof at === "object")
4105
+ addKeys(at);
4106
+ else if (!path.includes("."))
4107
+ addKeys(r);
4108
+ }
4109
+ }
4110
+ return Array.from(set);
4111
+ }
3738
4112
  // src/catalog/storage/storageCapabilities.ts
3739
4113
  var contentTypeField = {
3740
4114
  key: "contentType",
@@ -3804,7 +4178,18 @@ var STORAGE_PROVIDERS = {
3804
4178
  operations: [
3805
4179
  { value: "list", label: "List files", path: { key: "folderId", label: "Folder", selectMode: "folder" }, fields: [{ key: "query", label: "Search query", type: "string", advanced: true }, { key: "pageSize", label: "Max results", type: "number", defaultValue: 50, advanced: true }] },
3806
4180
  { value: "download", label: "Download file", path: { key: "fileId", label: "File", selectMode: "both", required: true } },
3807
- { value: "upload", label: "Upload file", path: { key: "folderId", label: "Destination folder", selectMode: "both" }, content: true, fields: [{ key: "fileName", label: "File name", type: "string", required: true }, { key: "mimeType", label: "Content type", type: "string", advanced: true }] },
4181
+ {
4182
+ value: "upload",
4183
+ label: "Upload file",
4184
+ path: { key: "folderId", label: "Destination folder", selectMode: "both" },
4185
+ content: true,
4186
+ fields: [
4187
+ { key: "fileName", label: "File name", type: "string", required: true },
4188
+ { key: "folderName", label: "Folder path", type: "string", advanced: true, placeholder: "Reports/2026", hint: "Folders under My Drive, separated by /. Missing ones are created on the first run. Ignored when a destination folder is picked." },
4189
+ { key: "ifExists", label: "If the file already exists", type: "select", options: [{ label: "Replace it (Drive keeps the versions)", value: "overwrite" }, { label: "Skip it", value: "skip" }, { label: "Keep both (rename)", value: "rename" }], defaultValue: "overwrite", advanced: true },
4190
+ { key: "mimeType", label: "Content type", type: "string", advanced: true }
4191
+ ]
4192
+ },
3808
4193
  { value: "delete", label: "Delete file", path: { key: "fileId", label: "File", selectMode: "both", required: true } },
3809
4194
  { value: "move", label: "Move", path: { key: "fileId", label: "File", selectMode: "both", required: true }, dest: { key: "destFolderId", label: "Destination folder", selectMode: "both", required: true } },
3810
4195
  { value: "copy", label: "Copy", path: { key: "fileId", label: "File", selectMode: "both", required: true }, dest: { key: "destFolderId", label: "Destination folder", selectMode: "both" }, fields: [{ key: "fileName", label: "New name", type: "string" }] },
@@ -3923,7 +4308,13 @@ var AiAgentConfig = {
3923
4308
  schema: {
3924
4309
  agentId: { type: "string", required: true, description: "Public id of a saved agent (agt_…)" },
3925
4310
  input: { type: "text", required: false, description: "The prompt; may reference upstream data with {{ods://…}} tokens. Empty uses the first upstream output." },
3926
- overrides: { type: "object", required: false, description: "Per-step overrides: model, temperature, topP, maxTokens, reasoningEffort, verbosity, batchSize, instructions, instructionsMode, expectedSchema" }
4311
+ overrides: { type: "object", required: false, description: "Per-step overrides: model, temperature, topP, maxTokens, reasoningEffort, verbosity, batchSize, instructions, instructionsMode, expectedSchema" },
4312
+ maxRows: {
4313
+ type: "number",
4314
+ required: false,
4315
+ min: 1,
4316
+ description: "Per-row mode: the most model calls this step may make, one per upstream row (default 50, at most 1000). More rows stop the step before any call."
4317
+ }
3927
4318
  },
3928
4319
  validate: (config) => {
3929
4320
  const errors = [];
@@ -3932,6 +4323,9 @@ var AiAgentConfig = {
3932
4323
  if (config.overrides !== undefined && (typeof config.overrides !== "object" || config.overrides === null || Array.isArray(config.overrides))) {
3933
4324
  errors.push("overrides must be an object");
3934
4325
  }
4326
+ if (config.maxRows !== undefined && !(Number.isInteger(config.maxRows) && config.maxRows >= 1 && config.maxRows <= 1000)) {
4327
+ errors.push("maxRows must be a whole number from 1 to 1000");
4328
+ }
3935
4329
  return { valid: errors.length === 0, errors };
3936
4330
  },
3937
4331
  merge: (userConfig) => ({ ...AiAgentConfig.defaults, ...userConfig }),
@@ -5462,6 +5856,7 @@ var CONFIG_CONTRACTS = {
5462
5856
  }
5463
5857
  },
5464
5858
  transform: { schema: TransformConfig.schema, validate: asValidator(TransformConfig.validate) },
5859
+ aggregate: { schema: AggregateConfig.schema, validate: asValidator(AggregateConfig.validate) },
5465
5860
  condition: { schema: ConditionConfig.schema, validate: asValidator(ConditionConfig.validate) },
5466
5861
  for_each: { schema: ForEachConfig.schema, validate: asValidator(ForEachConfig.validate) },
5467
5862
  file_operation: { schema: FileOperationConfig.schema, validate: asValidator(FileOperationConfig.validate) },
@@ -5813,9 +6208,12 @@ function mappingTransformFunctionName(transform) {
5813
6208
  return match ? match[1] : null;
5814
6209
  }
5815
6210
  export {
6211
+ AGGREGATE_FUNCTIONS,
6212
+ AGGREGATE_FUNCTION_DESCRIPTIONS,
5816
6213
  ActionCategory,
5817
6214
  ActionStatus,
5818
6215
  ActionType,
6216
+ AggregateConfig,
5819
6217
  AggregateFunction,
5820
6218
  AiAgentConfig,
5821
6219
  CONDITION_OPERATORS,
@@ -5887,6 +6285,7 @@ export {
5887
6285
  contractFor,
5888
6286
  contractVocabularyDrift,
5889
6287
  declaredKeys,
6288
+ detectRecordFields,
5890
6289
  emailDefinition,
5891
6290
  fileEditErrors,
5892
6291
  fileFormatErrors,
@@ -5904,6 +6303,7 @@ export {
5904
6303
  getNodeConfigStatus,
5905
6304
  getNodeValidationStatus,
5906
6305
  getStorageProvider,
6306
+ groupKeyName,
5907
6307
  inferSchemaFromSample,
5908
6308
  isEditorPrivateKey,
5909
6309
  isEventCapableConnector,
@@ -5912,6 +6312,7 @@ export {
5912
6312
  mappingTransformFunctionName,
5913
6313
  migrateDelayConfig,
5914
6314
  queryDefinition,
6315
+ resolveRecordSchema,
5915
6316
  selectsWithoutValues,
5916
6317
  storageConfigKeys,
5917
6318
  storageOperations,
@@ -70,10 +70,10 @@ export interface ConfigContract {
70
70
  };
71
71
  }
72
72
  /**
73
- * Every action in the catalog, keyed by action id — since 0.11.0 all 37
73
+ * Every action in the catalog, keyed by action id — since 0.11.0 all
74
74
  * definitions carry one (the last fifteen: group, ai_agent, idp_extract,
75
75
  * connector_trigger, the five file-format actions and the six storage
76
- * actions). `contractFor` is undefined only for an id the catalog does not
76
+ * actions; 0.13.0 added `aggregate`, the 38th). `contractFor` is undefined only for an id the catalog does not
77
77
  * know, and then consumers fall back to `inputSchema` alone. A new action is
78
78
  * a `config.ts` plus one line here; the drift test covers it from then on.
79
79
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octaviaflow/flow-rules",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "description": "The flow action catalog and the rules engine behind Flow Doctor — one definition of what a step accepts and what makes a flow valid, shared by the editor and the server",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",