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 }) => {
    // An agent call resolves to `{ answer, evaluation? }` — read `.answer`.
    const company = uses.enricher({
      prompt: `Which company owns the email domain of ${input.email}?`,
    }).answer;
    return { company, enriched: true };
  },
);
  • input / output are Zod schemas — they become the workflow’s form fields and end variables.
  • uses declares the tools, agents, and connectors this workflow calls (see below).
  • 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.

Reference tools, agents, and connectors

Everything a workflow calls that isn’t a native action — a tool, an agent, or a connector — is declared once in the header’s uses map and called by key in the body. Keeping the reference in the header (not inline in the body) lets the deploy order the dependency and inject the real uuid.
import { connectorRef } from "@cargo-ai/workflow-sdk";

defineWorkflow(
  "qualify-lead",
  {
    input: z.object({ email: z.string() }),
    output: z.unknown(),
    uses: {
      enrich: enrichTool, //     a defineTool handle
      sdr: sdrAgent, //          a defineAgent handle
      hubspot: hubspotConnector, // a defineConnector handle
      hunter: connectorRef("6f0c…", "hunter"), // …or a connector by uuid + slug
    },
  },
  ({ input, uses }) => {
    const verified = uses.enrich({ email: input.email }); // tool → call by key
    const contact = uses.hubspot.upsertContact({ email: input.email }); // connector → call an action
    return contact;
  },
);
  • Tools and agents are called directly — uses.<key>(input).
  • Connectors expose their actions — uses.<key>.<action>(input). Action names and their input/output are typed from the integration’s schema (run cargo-ai cdk types). The type comes from the integration slug, not the uuid — so the connector’s uuid can be a handle for a resource that doesn’t exist yet.
  • Pass a defineTool / defineAgent / defineConnector handle directly, or use toolRef / agentRef / connectorRef(uuid, slug) for a resource referenced by literal uuid.

What you can express in code

CapabilityIn code
Call a tool / agent / connectoruses.<key>(input) (tool / agent) or uses.<key>.<action>(input) (connector) — declared in uses
Native actionsnative.<action>(input)script, python, note, scoring
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.answer;
  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: () => uses.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: () => uses.lemlist.createLead({ campaignId: "cam_A", email: input.email }),
  right: () => uses.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([
  () => uses.hunter.findEmail({ domain: input.domain }),
  () => uses.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 uses.<key>.<action> registry
  • In code, cargo-ai cdk types writes the registry into .cargo-ai/, so a connector declared in uses (uses.<key>.<action>) autocompletes real action 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:
uses.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