npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cargo-ai/workflow-sdk

v1.0.6

Published

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

Readme

@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, delay, python, scoring, fileSearch) 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 cdk 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.

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 mapuses.<key>.<actionSlug>(input) — typed against the integration's synced action schemas (run cargo-ai cdk 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 milliseconds). 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()}` }}`.

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:

| Form | Example | | --------------------------------- | ------------------------------------------------------------------------------- | | Literal | 42, "alice", true, null, /regex/i | | Identifier in scope | input, a local const / let, a for…of loop variable | | Member access | input.score, enriched.email, rows[0] | | Optional chaining | enriched?.email | | Binary operators | a + b, a > b, a === b, a % b | | Logical operators | a && b, a \|\| b, a ?? b | | Unary operators | !a, -x, typeof a | | Ternary | a > 0 ? "yes" : "no" | | Template literal | `hi ${name}` | | Object / array literals | { a: 1, b: input.x }, [1, 2, ...rest] | | Method call on a value | email.includes("@"), /re/.test(s) | | Registry call | uses.<key>.<action>({...}), uses.validate(…), model.search(…), delay(…) | | ai("…") / js((rt) => …) | inline AI expression / inline script node | | Flow helper call | balance([…]), split(…), humanReview(…), memory.get(…) | | new of an engine-allowed global | new Date(s) |

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 cdk types, which reads your workspace over the API and writes:

  • .cargo-ai/cargo-types.d.tsdeclare 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 cdk 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 node | How you write it | | -------------------------- | -------------------------------------------------------------------------------- | | connector | uses.<key>.<actionSlug>({ … }) (declared via connectorRef / a CDK handle) | | tool | uses.<key>({ … }) (declared via toolRef / a CDK handle) | | agent | uses.<key>({ … }) (declared via agentRef / a CDK handle) | | native (storage) | model.search / upsert / update / insert / remove / record / ask / customColumn | | native (data helpers) | allocate / delay / python / scoring / fileSearch | | native.branch / switch | if / else if / else | | native.group | for (const x of items) { … } | | native.script | js((runtime) => …) | | native.balance | balance([() => …, () => …]) | | native.split | split(percentage, { left, right }) | | native.humanReview | humanReview(config, { approved?, declined? }) | | native.memory | memory.get / set / remove / increment / decrement / getOrSet | | start / end | implicit (defineWorkflow frames every workflow) |

Documentation

docs.getcargo.ai