@cargo-ai/workflow-sdk

中文文档
1.0.30发布于 2026/10/28UNLICENSEDTypeScriptES Module近 30 天下载 7.9万

Define Cargo workflows in TypeScript. Compiles to the same Node[] artifact the canvas produces and runs on the existing engine.

原始使用文档
文档目录16 节
  1. Deploying
  2. Tutorial
  3. 1. Define inputs and outputs
  4. 2. Call tools / agents / connectors / native helpers
  5. imports — reference build-time handles & constants by name
  6. Connectors in uses — the way to call a connector
  7. 3. Use idiomatic JS for control flow
  8. 4. Plain JS expressions ship verbatim
  9. 5. Inline JS via js(({ nodes }) => …)
  10. 6. Inline AI completions via ai("…")
  11. 7. Flow helpers — balance , split , humanReview , memory
  12. Supported JS
  13. Toolchain requirements
  14. Populating the registries
  15. Status
  16. Documentation

@cargo-ai/workflow-sdk

Define Cargo workflows in TypeScript.

import { z } from "zod";
import { defineWorkflow, connectorRef } from "@cargo-ai/workflow-sdk";

export default defineWorkflow(
  "name-and-domain-to-email",
  {
    input: z.object({
      first_name: z.string(),
      last_name: z.string(),
      domain: z.string(),
    }),
    output: z.object({ email: z.string().nullable() }),
    uses: {
      hunter: connectorRef("7eff455t-…", "hunter"),
      leadmagic: connectorRef("9c26d1f2-…", "leadmagic"),
    },
  },
  ({ input, uses }) => {
    const hunter = uses.hunter.findEmail({
      first_name: input.first_name,
      last_name: input.last_name,
      domain: input.domain,
    });
    const leadmagic = uses.leadmagic.findEmail({
      firstName: input.first_name,
      lastName: input.last_name,
      domain: input.domain,
    });
    return { email: hunter.email ?? leadmagic.email ?? null };
  },
);

The compiled output is a Node[] JSON artifact that the existing Cargo engine accepts via the same cargo-ai orchestration release deploy-draft CLI canvas users run today. Workflows authored in TypeScript and workflows authored on the canvas run identically.

Heads up: the body callback is parsed, not executed. The SDK reads the function's source, rewrites it into the engine's wire format, and ships it verbatim — so anything you write inside the body must be a supported JS expression (see Supported JS below). Closure variables, asynchronous code, throws, and console.log calls don't apply here.

Deploying

Deploy a workflow module straight from source with the CLI — it compiles the TypeScript and ships the resulting nodes as a new release:

# Preview compiled nodes + form fields (no API calls)
cargo-ai orchestration release deploy-draft --file ./my-workflow.ts --dry-run

# Deploy to an existing workflow (minor version auto-bumped)
cargo-ai orchestration release deploy-draft --file ./my-workflow.ts \
  --slug name-and-domain-to-email

The CLI uses --slug (or --workflow-uuid) to target the right workflow. It then runs the module through defineWorkflow, emits the Node[] graph, and POSTs it to the same release endpoint the canvas uses.

Tutorial

1. Define inputs and outputs

defineWorkflow(slug, options, build) is the only entry point. options.input is a Zod object schema that becomes the workflow's form fields (one per top-level key). options.output is a Zod schema describing the workflow's return shape. The build callback receives a scope handle and returns the workflow's result.

defineWorkflow(
  "score-lead",
  {
    input: z.object({
      first_name: z.string(),
      score: z.number(),
    }),
    output: z.string().nullable(),
  },
  ({ input }) => input.first_name,
);

2. Call tools / agents / connectors / native helpers

The scope handle exposes the model.* storage namespace and the standalone native helpers (allocate, classify, delay, python, scoring, fileSearch, writeFile, readFile, getFileUrl, sendEmail, waitEmailEvent, listEmailEvents) plus the input handle (input), the uses map (tools/agents/connectors you declared in the header — see below), the inline ai() helper, the js() escape hatch, and the flow helpers (balance, split, humanReview, memory — see section 7). There is no native slug registry and no integrations registry in the scope. Each call returns a typed handle you can dot-access — but the call itself is captured at parse time and emitted as a workflow node:

defineWorkflow(
  "find-and-verify",
  {
    input: z.object({ first_name: z.string(), domain: z.string() }),
    output: z.string().nullable(),
    uses: {
      hunter: connectorRef("9c26d1f2-…", "hunter"), // a connector, by handle
      validate: toolRef("3e0c5fde-…"), // a tool, by handle (see §2)
    },
  },
  ({ input, uses }) => {
    const found = uses.hunter.findEmail({
      first_name: input.first_name,
      domain: input.domain,
    });
    const verified = uses.validate({ email: found.email });
    return verified.is_safe ? found.email : null;
  },
);

The example above emits two nodes (a hunter.findEmail connector and a validate tool) wired into a chain ending at the workflow's end node. Tools, agents, and connectors have no slug registry — reference them by handle through uses (next). Each uses.<key> call is typed to the referenced resource's input and output — uses.validate({ email }) requires email and returns a Ref you can dot-access (verified.is_safe).

Native platform actions are called the same way, through their hand-written surfaces — the model.* storage namespace and the standalone helpers:

({ input, model, allocate }) => {
  const accounts = model.search({ modelUuid: input.modelUuid, limit: 50 });
  const result = allocate({
    recordId: input.recordId,
    type: "territory",
    territoryUuid: input.territoryUuid,
  });
  return result.member;
};

Note: there is no runtime integrations body registry to populate — connectors are called by handle through uses, not through a bundled registry. Run cargo-ai project types to generate the Integrations types for your workspace, so a connector declared in uses autocompletes its real actions (see Populating the registries). The platform's native actions are hand-written, strongly-typed surfaces that ship with the SDK (model.* and the standalone helpers) — no sync needed. Tools, agents, and connectors are all referenced by handle via uses.

The tools / agents registry keys are template slugs — workspace tools and agents have no slug of their own, only the template they were created from. To reference a specific tool / agent (a workspace instance with no usable template slug, or a @cargo-ai/cdk defineTool / defineAgent handle), declare it in the workflow's uses and call it by key — so the uuid stays in the header, not scattered through the body:

import { toolRef, agentRef } from "@cargo-ai/workflow-sdk";

defineWorkflow(
  "qualify",
  {
    input: z.object({ email: z.string() }),
    output: z.object({ verified: z.unknown(), answer: z.unknown() }),
    uses: {
      validate: toolRef("3e0c5fde-9f0e-4f60-b4cb-8a4e29d8a001"),
      qualify: agentRef("7b7a2f64-2f5c-49c0-9c19-d6a01a2b4c11"),
    },
  },
  ({ input, uses }) => {
    const verified = uses.validate({ email: input.email });
    const answer = uses.qualify({ prompt: `qualify ${input.email}` });
    return { verified, answer };
  },
);

uses accepts any { uuid, resource } handle. toolRef(uuid) / agentRef(uuid) are the escape hatch for a raw uuid; a @cargo-ai/cdk handle is already such a ref, so pass it directly (uses: { enrich: enrichTool }) — the deploy then orders the dependency and injects the real uuid.

imports — reference build-time handles & constants by name

uses covers the resources you call (tools, agents, connectors). But model.* / allocate take a uuid, not a callable — so a model, territory, or capacity handle can't ride in uses. And because the body is parsed from source (Function.prototype.toString()), a bare closure reference like accounts.uuid is unresolvable: the parser only sees the name accounts, and JavaScript has no way to read a stringified function's closure. Referencing it throws identifier 'accounts' is not in scope.

imports closes that gap. List the external values the body references, keyed by the identifier as written:

import { defineModel, defineTerritory } from "@cargo-ai/cdk";

const accounts = defineModel("accounts", {
  /* … */
});
const inbound = defineTerritory("inbound", {
  /* … */
});

defineWorkflow(
  "route-inbound",
  {
    input: z.object({ recordId: z.string(), domain: z.string() }),
    output: z.unknown(),
    imports: { accounts, inbound },
  },
  ({ input, model, allocate }) => {
    model.upsert({
      modelUuid: accounts.uuid, // deferred token — deploy-ordered & resolved
      matchingColumnSlug: "domain",
      matchingValue: input.domain,
    });
    return allocate({
      recordId: input.recordId,
      type: "territory",
      territoryUuid: inbound.uuid,
    });
  },
);

A @cargo-ai/cdk handle's .uuid stays a deferred token: it serializes verbatim into the node config, so the host orders the dependency (accounts deploys first) and swaps in the real uuid at deploy — exactly like uses. Plain constants (imports: { limit: 50 }) inline as literals, in both config positions and computed expressions.

Because the body still references the real outer bindings, TypeScript type-checks accounts.uuid against the actual handle — imports is a pure runtime pass-through. Two limits: a deferred token can only be used directly as a config value (not inside a {{ }} expression / template literal, where the host's deploy-time resolution never reaches — the parser rejects it), and imports is honored by the SDK parser, so a bare identifier works only when the value is listed there.

Connectors in uses — the way to call a connector

A connector is always declared in uses and called as uses.<key>.<action>(input) — there is no integrations body registry. Declare it with a @cargo-ai/cdk defineConnector handle or with connectorRef(uuid, integrationSlug), then call its actions by key. This pins that specific connector instance (its uuid rides on the emitted node instead of being resolved from the slug at run time) — which is exactly what you want when there are two HubSpot connectors (prod + sandbox), when you need a reproducible pin, or when you're wiring a CDK handle:

import { connectorRef } from "@cargo-ai/workflow-sdk";

defineWorkflow(
  "sync-contact",
  {
    input: z.object({ email: z.string() }),
    output: z.object({ ok: z.unknown() }),
    uses: {
      // a specific connector — or pass a `defineConnector` handle directly
      hs: connectorRef("9c26d1f2-…", "hubspot"),
    },
  },
  ({ input, uses }) => {
    // a connector exposes its actions: `uses.<key>.<actionSlug>(input)`
    const ok = uses.hs.upsertContact({ email: input.email });
    return { ok };
  },
);

Unlike a tool/agent (a single callable uses.<key>(input)), a connector is an action map — uses.<key>.<actionSlug>(input) — typed against the integration's synced action schemas (run cargo-ai project types) when the slug is known, or a loose map otherwise. A @cargo-ai/cdk defineConnector handle is already a connector ref, so uses: { hs: hubspot } works and the deploy orders the connector before this workflow and injects its real uuid.

Per-call options — retry + fallback. Any registry call takes an optional second argument to set a node's retry policy and/or let a failure continue instead of halting the run:

// uses: { hunter: connectorRef("9c26d1f2-…", "hunter") }
({ input, uses }) => {
  // retry up to 3 times; on final failure, continue to the next node
  const email = uses.hunter.findEmail(
    { domain: input.domain },
    { retry: 3, continueOnFailure: true },
  );
  return email;
};

retry is either a positive integer (shorthand for maximumAttempts) or { maximumAttempts?, initialInterval?, backoffCoefficient? } (intervals in seconds). continueOnFailure: true routes a failure to the node's normal successor (the last node falls through to the workflow's end) instead of stopping the run. Both values must be static literals. The same (input, options) shape works on uses.*, model.*, the native helpers, and memory.*.

3. Use idiomatic JS for control flow

The parser lowers familiar JS control-flow constructs into the corresponding native nodes:

  • if (a) { … } else { … } → branch node
  • if (a) … else if (b) … else … → switch node with one route per else if
  • for (const item of array) { … } → group node iterating over array
  • return expr inside a block ends that branch with expr
defineWorkflow(
  "qualify",
  {
    input: z.object({ score: z.number() }),
    output: z.string().nullable(),
  },
  ({ input, ai }) => {
    if (input.score > 80) {
      return ai("write a VIP greeting");
    } else if (input.score > 50) {
      return ai("write a standard greeting");
    } else {
      return null;
    }
  },
);

4. Plain JS expressions ship verbatim

Anywhere you'd write a value — an argument to an integration call, the right-hand side of a const, a return expression, an if condition — you can use a plain JS expression built from the supported subset listed below. The SDK rewrites identifiers to the matching nodes.<slug> path and ships the printed source string to the engine as {{ <expr> }}.

defineWorkflow(
  "compose",
  {
    input: z.object({ first: z.string(), last: z.string() }),
    output: z.string(),
  },
  ({ input }) => `${input.first.toLowerCase()} ${input.last.toLowerCase()}`,
);

The body above compiles to a single end.variables[0] whose value is {{ \${nodes.start.first.toLowerCase()} ${nodes.start.last.toLowerCase()}` }}`.

A few globals are usable directly: Date, Math, JSON, Number, String, Boolean, Array, Object, RegExp (e.g. new Date(), Math.random()). They're evaluated per record at run time, so new Date() in a mapping is each record's write time. Other globals (crypto, process, …) aren't available and are rejected at compile time.

5. Inline JS via js(({ nodes }) => …)

For logic that doesn't fit the supported expression subset (custom array manipulation, multi-statement transforms, etc.), emit a native.script node with js():

defineWorkflow(
  "shape",
  {
    input: z.object({}),
    output: z.array(z.object({ x: z.number() })),
    uses: { hunter: connectorRef("9c26d1f2-…", "hunter") },
  },
  ({ uses, js }) => {
    const search = uses.hunter.findEmail({});
    return js(({ nodes }) => {
      return nodes.search.results.map((r: { y: number }) => ({ x: r.y }));
    });
  },
);

js() ships the function's body verbatim. The function may not be async, and the runtime exposes only the { nodes, parentNodes } arguments — closure variables are not in scope.

6. Inline AI completions via ai("…")

ai() is shorthand for an inline AI-completed expression. It accepts a single template string or a string-concatenation expression and emits a templateExpression with instructTo: "ai". The engine runs it through the same AI pipeline the canvas does.

defineWorkflow(
  "describe",
  {
    input: z.object({ company: z.string() }),
    output: z.string().nullable(),
  },
  ({ input, ai }) => ai(`write a one-sentence description of ${input.company}`),
);

7. Flow helpers — balance, split, humanReview, memory

Some engine nodes have no JS-syntax equivalent, so the scope exposes them as helper functions the parser recognizes by name. Like everything else in the body, the calls are parsed, not executed — route bodies must be inline zero-argument arrow / function literals.

balance([...]) — round-robin. Each record runs exactly one route; the return value coalesces every route's output:

// uses: { hunter: connectorRef("9c26d1f2-…", "hunter"), leadmagic: connectorRef("9c26d1f2-…", "leadmagic") }
({ input, uses, balance }) => {
  const found = balance([
    () => uses.hunter.findEmail({ domain: input.domain }),
    () =>
      uses.leadmagic.findEmail({
        firstName: input.first,
        lastName: input.last,
      }),
  ]);
  return found;
};

split(percentage, { left, right }) — random A/B split. Records roll ∈ [0, 100); rolls below percentage run left, the rest run right:

// uses: { hunter: connectorRef("9c26d1f2-…", "hunter"), leadmagic: connectorRef("9c26d1f2-…", "leadmagic") }
({ uses, split }) =>
  split(30, {
    left: () => uses.hunter.findEmail({}),
    right: () => uses.leadmagic.findEmail({ firstName: "a", lastName: "b" }),
  });

humanReview(config, bodies?) — posts a Slack message and blocks until a reviewer clicks Approve or Decline (or the timeout fires). Optional approved / declined bodies run on the matching outcome:

// uses: { hunter: connectorRef("9c26d1f2-…", "hunter") }
({ input, uses, humanReview }) =>
  humanReview(
    {
      connectorUuid: "<slack-connector-uuid>",
      channelId: "C0123456789",
      title: "Approve outreach?",
      content: input.email,
    },
    {
      approved: () => uses.hunter.verifyEmail({ email: input.email }),
    },
  );

memory.<action>(opts) — workflow / workspace key-value store. Actions: get, set, remove, increment, decrement, getOrSet. Each call emits one node and returns a Ref<{ result: T }>:

({ memory }) => {
  const count = memory.increment({
    key: "emails_sent",
    expiresIn: { value: 1, unit: "day" },
  });
  return count.result;
};

expiresIn is required for set, getOrSet, increment, and decrement; scope defaults to "workflow" (pass "workspace" to share across workflows).

Supported JS

The body callback is parsed. Each expression position must be one of:

FormExample
Literal42, "alice", true, null, /regex/i
Identifier in scopeinput, a local const / let, a for…of loop variable
Member accessinput.score, enriched.email, rows[0]
Optional chainingenriched?.email
Binary operatorsa + b, a > b, a === b, a % b
Logical operatorsa && b, a || b, a ?? b
Unary operators!a, -x, typeof a
Ternarya > 0 ? "yes" : "no"
Template literal`hi ${name}`
Object / array literals{ a: 1, b: input.x }, [1, 2, ...rest]
Method call on a valueemail.includes("@"), /re/.test(s)
Registry calluses.<key>.<action>({...}), uses.validate(…), model.search(…), delay(…)
ai("…") / js((rt) => …)inline AI expression / inline script node
Flow helper callbalance([…]), split(…), humanReview(…), memory.get(…)
new / member call on an engine globalnew Date(s), Math.random(), JSON.stringify(x)

Statement-level forms supported inside the body:

  • const / let declarations binding a single identifier (no destructuring)
  • if / else if / else
  • for (const x of array) { … }
  • return <expr> / return;

Anything else (closures, try/catch, await, throw, function declarations, destructuring) is rejected at compile time with a clear error.

Toolchain requirements

The SDK reads your workflow body with Function.prototype.toString() and parses that source. The function the JS engine holds must therefore be the code you wrote:

  • Compile workflow files with a modern target (ES2022 or later). Down-leveling rewrites for…of, template literals, and optional chaining into forms the parser rejects — and worse, into code you didn't write.
  • Don't instrument workflow files. Code-coverage tools (istanbul/nyc, vitest's default V8 coverage is fine) inject counters into the function source and break parsing. Exclude workflow definition files from instrumentation.
  • Standard TypeScript type syntax is fine — tsc and esbuild strip types without touching the runtime code.

If the body can't be parsed, defineWorkflow throws a could not parse the workflow body error explaining this constraint.

Populating the registries

Your workspace's connector types come from cargo-ai project types, which reads your workspace over the API and writes:

  • .cargo-ai/cargo-types.d.ts — declare module augmentations. It adds your workspace's integration slugs to the Integrations interface (with input types generated from each action's schema) — now used to type uses.<key>.<action>({ … }) so a connector declared in uses typechecks against the real shape — and also types @cargo-ai/cdk's defineConnector/defineModel config against your connector/extractor schemas

Add .cargo-ai to your tsconfig.json include with an explicit glob (".cargo-ai/**/*.d.ts" — a bare dot-dir path is ignored by TypeScript). Re-run cargo-ai project types whenever the workspace's connectors change.

Tools and agents are not synced — they're referenced by handle through defineWorkflow's uses (a @cargo-ai/cdk defineTool/defineAgent handle, or toolRef/agentRef("<uuid>")). The platform's native actions are not synced either: they're identical in every workspace, so they ship with the SDK as hand-written, strongly-typed surfaces (model.* and the standalone helpers).

Status

Early access. Ships defineWorkflow plus the hand-written native surfaces (model.* and the standalone helpers) and the handle-based uses surface (tools, agents, connectors) described above. Canvas-side rendering of SDK-authored workflows works because the wire format is identical.

Node types the SDK can express today:

Engine nodeHow you write it
connectoruses.<key>.<actionSlug>({ … }) (declared via connectorRef / a project resource handle)
tooluses.<key>({ … }) (declared via toolRef / a project resource handle)
agentuses.<key>({ … }) (declared via agentRef / a project resource handle)
native (storage)model.search / upsert / update / insert / remove / record / ask / customColumn
native (data helpers)allocate / classify / delay / python / scoring / fileSearch / writeFile / readFile / getFileUrl / sendEmail / waitEmailEvent / listEmailEvents
native.branch / switchif / else if / else
native.groupfor (const x of items) { … }
native.scriptjs((runtime) => …)
native.balancebalance([() => …, () => …])
native.splitsplit(percentage, { left, right })
native.humanReviewhumanReview(config, { approved?, declined? })
native.memorymemory.get / set / remove / increment / decrement / getOrSet
start / endimplicit (defineWorkflow frames every workflow)

Documentation

docs.getcargo.ai

版本与发布记录

@cargo-ai/workflow-sdk 共发布 30 个版本,首次发布于 2026/06/10;过去 12 个月发布了 30 个版本。

版本发布日期距今
1.0.30latest2026/10/2811 天前
1.0.292026/10/2317 天前
1.0.282026/10/2119 天前
1.0.272026/10/1921 天前
1.0.262026/10/051 个月前
1.0.252026/09/251 个月前

关于 @cargo-ai/workflow-sdk 的常见问题

@cargo-ai/workflow-sdk 是什么?

Define Cargo workflows in TypeScript. Compiles to the same Node[] artifact the canvas produces and runs on the existing engine.

如何安装 @cargo-ai/workflow-sdk?

在项目目录运行 npm install @cargo-ai/workflow-sdk,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:zod(^4.4.3)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@cargo-ai/workflow-sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cargo-ai/workflow-sdk 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@cargo-ai/workflow-sdk 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@cargo-ai/workflow-sdk 可以商用吗?

@cargo-ai/workflow-sdk 使用 UNLICENSED 许可。作者声明为 UNLICENSED,即不授予公开使用许可(与公共领域的 Unlicense 不同)。使用前需获得作者授权。以上为概括,不构成法律意见,请以许可原文为准。

@cargo-ai/workflow-sdk 的下载量有多少?

根据 npm 官方下载统计,@cargo-ai/workflow-sdk 在 2026/10/09 – 2026/11/07 这 30 天内被下载 79,396 次,日均约 2,647 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cargo-ai/workflow-sdk 还在维护吗?

最新版本 1.0.30 发布于 2026/10/28(11 天前);至今共发布 30 个版本,其中过去 12 个月发布了 30 个;首次发布于 2026/06/10。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。