@agentic-surfaces/core 0.1.1 → 0.1.3

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.
@@ -0,0 +1,2 @@
1
+ import type { NodeHandler } from "../types.js";
2
+ export declare const foreachHandler: NodeHandler;
@@ -0,0 +1,29 @@
1
+ import jsonata from "jsonata";
2
+ import { buildJsonataScope } from "./transform.js";
3
+ export const foreachHandler = {
4
+ type: "task.foreach",
5
+ async execute(node, ctx) {
6
+ if (!ctx.services.runWorkflow) {
7
+ throw new Error("sub-workflow invocation not configured");
8
+ }
9
+ const itemsExpr = String(node.config.items ?? "");
10
+ const workflowName = String(node.config.workflow ?? "");
11
+ const items = await jsonata(itemsExpr).evaluate(buildJsonataScope(ctx));
12
+ if (!Array.isArray(items)) {
13
+ throw new Error("task.foreach: items expression did not evaluate to an array");
14
+ }
15
+ const results = [];
16
+ for (const item of items) {
17
+ try {
18
+ const subOutputs = await ctx.services.runWorkflow(workflowName, item);
19
+ results.push({ ok: true, item, outputs: Object.fromEntries(subOutputs) });
20
+ }
21
+ catch (err) {
22
+ const errorMessage = err instanceof Error ? err.message : String(err);
23
+ ctx.services.logger?.error?.("foreach item failed", { error: errorMessage });
24
+ results.push({ ok: false, item, error: errorMessage });
25
+ }
26
+ }
27
+ return { output: results };
28
+ },
29
+ };
@@ -15,7 +15,24 @@ export const httpHandler = {
15
15
  async execute(node, ctx) {
16
16
  const cfg = node.config;
17
17
  const method = (cfg.method ?? "GET").toUpperCase();
18
- const url = new URL(resolveSecrets(cfg.url));
18
+ // urlExpression is jsonata over the run scope (per-item URLs from earlier
19
+ // outputs / the trigger payload); env vars in its result are resolved after.
20
+ // Plain `url` is env-substituted only. Exactly one must be provided.
21
+ let rawUrl;
22
+ if (cfg.urlExpression) {
23
+ const evaluated = await jsonata(cfg.urlExpression).evaluate(buildJsonataScope(ctx));
24
+ if (typeof evaluated !== "string") {
25
+ throw new Error("urlExpression must evaluate to a string");
26
+ }
27
+ rawUrl = resolveSecrets(evaluated);
28
+ }
29
+ else if (cfg.url) {
30
+ rawUrl = resolveSecrets(cfg.url);
31
+ }
32
+ else {
33
+ throw new Error("task.http requires either url or urlExpression");
34
+ }
35
+ const url = new URL(rawUrl);
19
36
  for (const [k, v] of Object.entries(resolveObj(cfg.query)))
20
37
  url.searchParams.set(k, v);
21
38
  if (ctx.services.dryRun && method !== "GET" && method !== "HEAD") {
@@ -0,0 +1,2 @@
1
+ import type { NodeHandler } from "../types.js";
2
+ export declare const runWorkflowHandler: NodeHandler;
@@ -0,0 +1,18 @@
1
+ import jsonata from "jsonata";
2
+ import { buildJsonataScope } from "./transform.js";
3
+ export const runWorkflowHandler = {
4
+ type: "task.run-workflow",
5
+ async execute(node, ctx) {
6
+ if (!ctx.services.runWorkflow) {
7
+ throw new Error("sub-workflow invocation not configured");
8
+ }
9
+ const workflowName = String(node.config.workflow ?? "");
10
+ const payloadExpression = node.config.payloadExpression;
11
+ let payload = null;
12
+ if (payloadExpression && typeof payloadExpression === "string" && payloadExpression.length > 0) {
13
+ payload = await jsonata(payloadExpression).evaluate(buildJsonataScope(ctx));
14
+ }
15
+ const outputs = await ctx.services.runWorkflow(workflowName, payload);
16
+ return { output: Object.fromEntries(outputs) };
17
+ },
18
+ };
package/dist/index.d.ts CHANGED
@@ -7,3 +7,5 @@ export * from "./cache.js";
7
7
  export * from "./observer.js";
8
8
  export * from "./secrets.js";
9
9
  export * from "./project-config.js";
10
+ export * from "./handlers/foreach.js";
11
+ export * from "./handlers/run-workflow.js";
package/dist/index.js CHANGED
@@ -7,3 +7,5 @@ export * from "./cache.js";
7
7
  export * from "./observer.js";
8
8
  export * from "./secrets.js";
9
9
  export * from "./project-config.js";
10
+ export * from "./handlers/foreach.js";
11
+ export * from "./handlers/run-workflow.js";
@@ -8,6 +8,13 @@ export declare function runWorkflowOnce(workflow: Workflow, opts: {
8
8
  payload?: unknown;
9
9
  observer?: RunObserver;
10
10
  }): Promise<Map<string, unknown>>;
11
+ export declare function buildWorkflowRunner(opts: {
12
+ workflows: Map<string, Workflow>;
13
+ registry: HandlerRegistry;
14
+ services: Services;
15
+ observer?: RunObserver;
16
+ maxDepth?: number;
17
+ }): (name: string, payload: unknown) => Promise<Map<string, unknown>>;
11
18
  export declare class Scheduler {
12
19
  private services;
13
20
  private registry;
package/dist/scheduler.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { Cron } from "croner";
2
2
  import { HandlerRegistry } from "./registry.js";
3
- import { runWorkflow } from "./executor.js";
3
+ import { runWorkflow as runWorkflowExec } from "./executor.js";
4
4
  import { transformHandler } from "./handlers/transform.js";
5
5
  import { branchHandler } from "./handlers/branch.js";
6
6
  import { httpHandler } from "./handlers/http.js";
7
7
  import { agentHandler } from "./handlers/agent.js";
8
+ import { foreachHandler } from "./handlers/foreach.js";
9
+ import { runWorkflowHandler } from "./handlers/run-workflow.js";
8
10
  import { ConsoleObserver } from "./observer.js";
9
11
  const passthrough = (type) => ({
10
12
  type,
@@ -20,6 +22,9 @@ export function defaultRegistry() {
20
22
  r.register(agentHandler);
21
23
  r.register(passthrough("trigger.cron"));
22
24
  r.register(passthrough("trigger.webhook"));
25
+ r.register(passthrough("trigger.command"));
26
+ r.register(foreachHandler);
27
+ r.register(runWorkflowHandler);
23
28
  return r;
24
29
  }
25
30
  export async function runWorkflowOnce(workflow, opts) {
@@ -28,7 +33,23 @@ export async function runWorkflowOnce(workflow, opts) {
28
33
  ?? workflow.nodes.find(n => n.type.startsWith("trigger."))?.id;
29
34
  if (!triggerNodeId)
30
35
  throw new Error(`workflow ${workflow.name} has no trigger node`);
31
- return runWorkflow({ workflow, triggerNodeId, registry, services: opts.services, payload: opts.payload, observer: opts.observer });
36
+ return runWorkflowExec({ workflow, triggerNodeId, registry, services: opts.services, payload: opts.payload, observer: opts.observer });
37
+ }
38
+ export function buildWorkflowRunner(opts) {
39
+ const { workflows, registry, observer, maxDepth = 10 } = opts;
40
+ let depth = 0;
41
+ function runWorkflow(name, payload) {
42
+ const wf = workflows.get(name);
43
+ if (!wf)
44
+ throw new Error(`sub-workflow not found: "${name}"`);
45
+ if (depth >= maxDepth)
46
+ throw new Error(`max workflow depth exceeded (possible cycle): depth=${depth}`);
47
+ depth++;
48
+ const subServices = { ...opts.services, runWorkflow };
49
+ return runWorkflowOnce(wf, { registry, services: subServices, observer, payload })
50
+ .finally(() => { depth--; });
51
+ }
52
+ return runWorkflow;
32
53
  }
33
54
  export class Scheduler {
34
55
  services;
package/dist/schema.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { z } from "zod";
2
2
  import { parse as parseYaml } from "yaml";
3
3
  const nodeType = z.enum([
4
- "trigger.cron", "trigger.webhook",
4
+ "trigger.cron", "trigger.webhook", "trigger.command",
5
5
  "task.http", "task.transform", "task.branch",
6
+ "task.run-workflow", "task.foreach",
6
7
  "agent.run",
7
8
  ]);
8
9
  const retry = z.object({ maxAttempts: z.number().int().min(1), backoffMs: z.number().int().min(0) });
@@ -38,6 +39,23 @@ const workflow = z.object({
38
39
  ctx.addIssue({ code: "custom", message: `node "${n.id}" (trigger.cron) requires a non-empty config.cron string` });
39
40
  }
40
41
  }
42
+ if (n.type === "task.run-workflow") {
43
+ const workflow = n.config.workflow;
44
+ if (typeof workflow !== "string" || workflow.trim() === "") {
45
+ ctx.addIssue({ code: "custom", message: `node "${n.id}" (task.run-workflow) requires a non-empty config.workflow string` });
46
+ }
47
+ }
48
+ if (n.type === "task.foreach") {
49
+ const cfg = n.config;
50
+ const items = cfg.items;
51
+ const wfName = cfg.workflow;
52
+ if (typeof items !== "string" || items.trim() === "") {
53
+ ctx.addIssue({ code: "custom", message: `node "${n.id}" (task.foreach) requires a non-empty config.items string` });
54
+ }
55
+ if (typeof wfName !== "string" || wfName.trim() === "") {
56
+ ctx.addIssue({ code: "custom", message: `node "${n.id}" (task.foreach) requires a non-empty config.workflow string` });
57
+ }
58
+ }
41
59
  }
42
60
  });
43
61
  export function loadWorkflow(yamlText) {
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { AgentRunner, McpServerRef } from "@agentic-surfaces/agent";
2
2
  export type { AgentRunner, McpServerRef };
3
- export type NodeType = "trigger.cron" | "trigger.webhook" | "task.http" | "task.transform" | "task.branch" | "agent.run";
3
+ export type NodeType = "trigger.cron" | "trigger.webhook" | "trigger.command" | "task.http" | "task.transform" | "task.branch" | "task.run-workflow" | "task.foreach" | "agent.run";
4
4
  export interface RetryPolicy {
5
5
  maxAttempts: number;
6
6
  backoffMs: number;
@@ -44,6 +44,7 @@ export interface Services {
44
44
  runner?: string;
45
45
  mcpServers?: McpServerRef[];
46
46
  };
47
+ runWorkflow?: (name: string, payload: unknown) => Promise<Map<string, unknown>>;
47
48
  }
48
49
  export interface RunContext {
49
50
  workflow: Workflow;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentic-surfaces/core",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -23,7 +23,7 @@
23
23
  "jsonata": "^2.2.1",
24
24
  "yaml": "^2.9.0",
25
25
  "zod": "^4.4.3",
26
- "@agentic-surfaces/agent": "0.1.1"
26
+ "@agentic-surfaces/agent": "0.1.3"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/better-sqlite3": "^7.6.13",