Skip to main content
The public form turns a tool into a hosted, embeddable web form. Visitors fill it in on your site, and each submission runs the tool’s workflow — like a Typeform that triggers a Cargo automation instead of just collecting data. It is the easiest way to put a tool behind a public surface (landing page, in-product survey, partner site) without writing a backend.

How it works

Every tool already has an Input node that defines its data schema (see Tools overview). When the public form is enabled, Cargo hosts a zero-dependency SDK (@cargo-ai/form-sdk) that reads those input fields and renders them as a styled form on your page. Each submission runs the same workflow that the Trigger tab, plays, and agents run — only the entry point changes.
The tool must be published for the public form to accept submissions. The form always renders the deployed version, not the working draft.

Enabling the public form

  1. Open your tool and go to the Trigger tab
  2. In the Public form section, click Configure
  3. Toggle Enable public form on
  4. Configure access, spam protection, and appearance (see below)
  5. Click the save icon
Once enabled, the Embed snippet section gives you a ready-to-paste <script> tag you can drop into any HTML page.

Access

Allowed origins

The form is gated by an explicit allow-list of browser origins. Requests from any other origin are rejected with 403.
ValueEffect
https://www.acme.comAllow exactly this origin
One origin per lineAllow each listed origin
*Allow any origin (use only for fully public widgets)
Origin must be an exact matchhttps://www.acme.com does not allow https://acme.com or https://staging.acme.com. List every host you embed on (including local dev URLs like http://localhost:3000).
Requests without an Origin header (server-to-server calls, curl) bypass the check.

Spam protection

Three layers run in order on every submission. Failing any one rejects the request with 400.

Honeypot

A hidden decoy field is injected automatically by the SDK. Real users never see it; most spam bots fill every field they find. If it is filled, the submission is dropped. No configuration needed.

Time-trap

The SDK stamps a render timestamp into the payload. Submissions arriving faster than Minimum fill time are rejected as bot traffic.
SettingRecommended
Minimum fill time (ms)1500 for short forms, 3000+ for longer ones
0Disables the time-trap

CAPTCHA

When configured, Cargo verifies a CAPTCHA token server-side before running the workflow.
ProviderWhere to get keys
Cloudflare TurnstileTurnstile dashboard
hCaptchahCaptcha dashboard
Configure both fields:
  • Site key — public, embedded on your page
  • Secret — server-side only, never sent to the browser
Pass the token from your page into the SDK via addHiddenFields:
<script>
  Cargo.loadForm("TOOL_UUID", {}, (form) => {
    turnstile.render("#captcha", {
      sitekey: "YOUR_SITE_KEY",
      callback: (token) => {
        form.addHiddenFields({ _cargo_captcha_token: token });
      },
    });
  });
</script>
Cargo also has a built-in per-tool, per-IP rate limit of 10 submissions per minute. You don’t configure it — it’s always on.

Appearance

The SDK ships a sensible default stylesheet, but you can theme the form per-workspace so every embed picks up your brand automatically.
SettingEffect
Color schemeAuto follows the visitor’s OS preference, or force Light / Dark
Primary colorSubmit button, input focus border and ring, checkbox accent; the button hover shade is derived from it automatically
Primary text colorText on top of the primary color (e.g. submit button label)
Border radiusAny CSS length (6px, 999px for fully rounded, …) applied to inputs and the button
Font familyAny CSS font-family value (e.g. "Inter", system-ui, sans-serif)
Embedders can still override any of these per page via the SDK’s theme option — workspace defaults are merged with per-call overrides, and explicit overrides win.
Leave a field blank to fall back to the SDK’s bundled default. The form automatically follows prefers-color-scheme unless you force a preset.

Embedding the form

The easiest path is the CDN snippet from the Embed snippet section:
<div data-cargo-form="TOOL_UUID"></div>
<script>
  window.Cargo = window.Cargo || {};
  Cargo.loadForm = Cargo.loadForm || function () {
    var call = { args: arguments };
    call.promise = new Promise(function (resolve, reject) {
      call.resolve = resolve;
      call.reject = reject;
    });
    (Cargo._q = Cargo._q || []).push(call);
    return call.promise;
  };

  Cargo.loadForm("TOOL_UUID");
</script>
<script src="https://cdn.getcargo.io/forms/v1.js" async></script>
That’s it — Cargo renders a styled form into the <div>, and runs your tool’s workflow on submit. The first few lines are a queueing stub: the CDN script loads with async, so it can finish loading before or after your inline code runs. The stub makes Cargo.loadForm safe to call immediately — calls are queued and replayed (and their promises resolved) as soon as the bundle loads. Don’t remove it, and don’t call Cargo.loadForm from an inline script without it.

npm

For SPA or framework projects:
npm install @cargo-ai/form-sdk
import { loadForm } from "@cargo-ai/form-sdk";

const form = await loadForm("TOOL_UUID", {
  target: "#cargo-form",
  mode: "sync",
});

form.onSuccess((values, response) => {
  if (response.outcome === "completed") {
    console.log("Workflow output:", response.result.output);
  }
});

Headless mode

Bring your own UI and let the SDK handle validation, anti-spam metadata and submission:
const form = await loadForm("TOOL_UUID", { render: "headless", mode: "async" });

form.setValues({ email: "ada@example.com" });
await form.submit();
You’re now in full control of the DOM — the SDK just handles the submission plumbing.

SDK reference

loadForm(toolUuid, options?, onReady?)

Loads the deployed schema and returns a FormInstance.
OptionDefaultDescription
render"render""render" builds the form DOM; "headless" skips DOM rendering
mode"sync""sync" waits for and returns the run output; "async" returns immediately and polls
target[data-cargo-form="TOOL_UUID"]Element or selector to render into
autoCaptureUtmtrueCapture UTM params + page URL automatically into hidden values
submitLabel"Submit"Label for the submit button
classPrefix"cargo-form"Prefix for every CSS class emitted by the renderer
injectStylestrue (render) / false (headless)Inject the bundled default stylesheet once per page
stylesheetUrlURL of a custom stylesheet to inject instead of the bundled one
themePer-call theme overrides (merged on top of workspace defaults)

FormInstance lifecycle

MethodPurpose
setValues(values)Merge values into the form (visible fields when rendered)
addHiddenFields(values)Add hidden values sent with the submission (UTMs, lead source, CAPTCHA token, …)
getValues()Current values (visible + hidden)
onValidate(handler)Register a validation hook; return false to block submission
onSubmit(handler)Called right before submit; mutate the returned values to change the payload
onSuccess(handler)Called after a successful submission; return false to suppress default behavior
onError(handler)Called when submission fails
submit()Programmatically submit (used by headless mode)
<script>
  Cargo.loadForm("TOOL_UUID", { mode: "sync" }, (form) => {
    form.addHiddenFields({ page_url: location.href });

    form.onValidate((values) => {
      return typeof values.email === "string" && values.email.includes("@");
    });

    form.onSuccess(() => {
      location.href = "/thank-you";
      return false;
    });
  });
</script>

Sync vs async submission

ModeReturnsUse when
"sync"Waits up to 60s, returns the workflow outputShort workflows (enrichment, scoring) — show the result
"async"Returns a runUuid immediately; SDK polls for statusLong workflows, or fire-and-forget submissions
If a sync run exceeds 60s it falls back to async automatically — onSuccess receives a { outcome: "pending", runUuid } response you can keep polling.

Privacy

The SDK is privacy-aware by default:
  • Respects Global Privacy Control (Sec-GPC: 1) and DNT: 1. When the visitor has opted out, no anonymous id is set and UTM auto-capture is skipped.
  • Form submission is always an explicit user action, so opt-out never blocks the submission itself — only passive identity stitching is suppressed.
UTM auto-capture (when enabled) reads utm_source, utm_medium, utm_campaign, utm_term, utm_content and page_url from the current URL and includes them as hidden values on submit.

Best practices

Use * only for widgets that genuinely run everywhere. For anything else, list each host explicitly — it stops other sites from embedding your form and burning your credits.
Honeypot + time-trap catch nearly all unsophisticated bots for free. Add Turnstile or hCaptcha when the form triggers credit-heavy work (AI nodes, enrichment) or feeds downstream systems like a CRM.
Browsers won’t wait long. If your tool routinely takes more than a few seconds (AI calls, multi-step enrichment), switch to mode: "async" and show a “we’ll be in touch” screen instead of a spinner.
Beyond UTMs, pass page_url, referrer, plan_tier, or anything else your workflow can branch on via form.addHiddenFields({ ... }). They’re sent as regular workflow inputs — define matching fields on the tool’s Input node.
Origin / CORS issues only surface in a real browser on a real domain. Always test the snippet on the page you’ll actually embed on, not just localhost.