Skip to main content
A workflow is the logic that a tool runs or a play executes per row. You author it in TypeScript with defineWorkflow from @cargo-ai/cdk (re-exported from @cargo-ai/workflow-sdk). Workflows authored in code and workflows drawn on the canvas compile to the same artifact and run identically — the code DSL is just a different authoring surface.

Define a workflow

import { defineWorkflow } from "@cargo-ai/cdk";
import { z } from "zod";
import { enricher } from "../agents/enricher";

export const enrichFlow = defineWorkflow(
  "enrich-contact",
  {
    input: z.object({ email: z.string() }),
    output: z.object({ company: z.string(), enriched: z.boolean() }),
    uses: { enricher }, // tools/agents this workflow calls, by handle
  },
  ({ input, uses, ai }) => {
    const company = uses.enricher({
      prompt: `Which company owns the email domain of ${input.email}?`,
    });
    return { company, enriched: true };
  },
);
  • input / output are Zod schemas — they become the workflow’s form fields and end variables.
  • uses forward-references tools and agents by handle so Cargo orders the deploy and injects real uuids.
  • The body is parsed, not executed. The SDK reads the function’s source and lowers it to workflow nodes; it never runs at build time. That’s why the body can’t be async and why only a subset of JavaScript is supported (see below) — anything that must run at runtime goes in a js() block.

What you can express in code

CapabilityIn code
Connector actionsintegrations.<slug>.<action>(input, { retry, continueOnFailure })
Native actionsnative.<action>(input)script, python, note, scoring
Call a tool / agentuses.<key>(input) (declared in uses)
Branchif (cond) { … } else { … }
Switchif … else if … else …
Loop over a listfor (const x of items) { … }
Inline AI valueai(\prompt $`)`
Custom JavaScriptjs(({ nodes }) => …)
Human approvalhumanReview(config, { approved, declined })
Memory (KV)memory.get / set / getOrSet / remove / increment / decrement
Traffic split / balancesplit(30, { left, right }), balance([...])
Supported statements: const / let (single identifier), if / else if / else, for…of, and return. Not supported: async/await, try/catch, throw, closures, nested function declarations, and destructuring assignments.

Built-in actions

ai() — inline AI value

Ask an LLM for a single value anywhere an expression fits:
const summary = ai(`Summarize what ${input.domain} sells in one sentence.`);

js() — runtime JavaScript

Escape hatch for logic the parser can’t lower — the function you pass does run at runtime, as a script node, with access to upstream node outputs:
const normalized = js(({ nodes }) => {
  const raw = nodes.enricher.company;
  return raw.trim().toLowerCase();
});
Use js() for transformations, parsing, and computation; keep orchestration (branches, loops, action calls) in the DSL where it compiles to inspectable nodes. native.python(...) is the same idea in Python.

humanReview() — human-in-the-loop gate

Posts a Slack message with Approve / Decline buttons and blocks the run until a reviewer clicks one (or the timeout fires — timeouts auto-decline):
humanReview(
  {
    connectorUuid: slack.uuid,        // the Slack connector to post through
    channelId: "C0123456789",
    title: "Approve outreach?",
    content: `Sending sequence to *${input.email}*`,
    timeoutMilliseconds: 86_400_000,  // default: 24 hours
    enableEditButton: true,           // default: true
  },
  {
    approved: () => integrations.lemlist.createLead({ campaignId: "cam_123", email: input.email }),
    declined: () => memory.increment({ key: "declined", expiresIn: { value: 30, unit: "day" } }),
  },
);

memory.* — key-value state

Persist state across runs without a model. Each call is one node and returns { result }:
const seen = memory.get<number>({ key: input.email });
if (seen.result === null) {
  memory.set({ key: input.email, value: 1, expiresIn: { value: 7, unit: "day" } });
}
  • scope: "workflow" (default) is private to this workflow; "workspace" is shared across all workflows.
  • set, getOrSet, increment, and decrement require expiresIn ({ value, unit } with second / minute / hour / day).
  • increment / decrement are atomic counters — the safe way to rate-limit or cap sends.

split() and balance() — traffic shaping

// A/B: ~30% of records run left, the rest right
split(30, {
  left: () => integrations.lemlist.createLead({ campaignId: "cam_A", email: input.email }),
  right: () => integrations.lemlist.createLead({ campaignId: "cam_B", email: input.email }),
});

// Round-robin: each record runs exactly ONE route; returns the first non-null output
const result = balance([
  () => integrations.hunter.findEmail({ domain: input.domain }),
  () => integrations.dropcontact.findEmail({ website: input.domain }),
]);
Route bodies must be inline zero-argument arrows.

The action catalog

Cargo ships 120+ actions across enrichment, CRM, AI, communication, and logic. Three ways to browse what’s callable:
cargo-ai connection integration list             # every integration and its slug
cargo-ai connection integration get hubspot      # one integration's actions + config schemas
cargo-ai cdk types                               # generate the typed integrations.* registry
  • In code, cargo-ai cdk types writes the registry into .cargo-ai/, so integrations.<slug>.<action> autocompletes real slugs and config shapes in your editor.
  • In the UI, the same catalog is the node palette in the tool editor — searchable by category, with per-field documentation inline.
  • Per integration, each page in the Integrations tab documents its actions, parameters, and credit costs.

Code covers a subset of the canvas

The code DSL is designed for linear and branching tool/play logic. It does not yet mirror the entire visual node palette. These are canvas-only today — build them in the tool editor if you need them:
  • Filter, Delay, and Variables nodes
  • Model actions — search, insert, update, upsert, remove, custom column
  • File search and Model ask standalone AI nodes
  • Allocate (lead routing)
Because both paths compile to the same node format, you can start a workflow in code and finish complex, canvas-only parts in the UI, or vice versa. But the CDK never reads UI edits back into your files — once you finish a workflow in the UI, re-deploying a changed code definition overwrites the canvas work. See Code and UI round-trips before mixing the two on one resource.

Retry and failure options

Every connector, tool, and agent call accepts a second options argument:
integrations.hunter.findEmail({ domain }, {
  retry: { maximumAttempts: 3, initialInterval: 1000, backoffCoefficient: 2 },
  continueOnFailure: true,
});

Deploying workflow changes

A workflow is deployed as part of the tool or play that owns it (cargo-ai cdk deploy). You can also push a Workflow SDK module to an existing workflow directly:
cargo-ai orchestration release deploy-draft --file ./enrichFlow.ts