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
- Open your tool and go to the Trigger tab
- In the Public form section, click Configure
- Toggle Enable public form on
- Configure access, spam protection, and appearance (see below)
- Click the save icon
<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 with403.
| Value | Effect |
|---|---|
https://www.acme.com | Allow exactly this origin |
| One origin per line | Allow each listed origin |
* | Allow any origin (use only for fully public widgets) |
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 with400.
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.| Setting | Recommended |
|---|---|
| Minimum fill time (ms) | 1500 for short forms, 3000+ for longer ones |
| 0 | Disables the time-trap |
CAPTCHA
When configured, Cargo verifies a CAPTCHA token server-side before running the workflow.| Provider | Where to get keys |
|---|---|
| Cloudflare Turnstile | Turnstile dashboard |
| hCaptcha | hCaptcha dashboard |
- Site key — public, embedded on your page
- Secret — server-side only, never sent to the browser
addHiddenFields:
Appearance
The SDK ships a sensible default stylesheet, but you can theme the form per-workspace so every embed picks up your brand automatically.| Setting | Effect |
|---|---|
| Color scheme | Auto follows the visitor’s OS preference, or force Light / Dark |
| Primary color | Submit button, input focus border and ring, checkbox accent; the button hover shade is derived from it automatically |
| Primary text color | Text on top of the primary color (e.g. submit button label) |
| Border radius | Any CSS length (6px, 999px for fully rounded, …) applied to inputs and the button |
| Font family | Any CSS font-family value (e.g. "Inter", system-ui, sans-serif) |
theme option — workspace defaults are merged with per-call overrides, and explicit overrides win.
Embedding the form
The easiest path is the CDN snippet from the Embed snippet section:<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:Headless mode
Bring your own UI and let the SDK handle validation, anti-spam metadata and submission:SDK reference
loadForm(toolUuid, options?, onReady?)
Loads the deployed schema and returns a FormInstance.
| Option | Default | Description |
|---|---|---|
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 |
autoCaptureUtm | true | Capture 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 |
injectStyles | true (render) / false (headless) | Inject the bundled default stylesheet once per page |
stylesheetUrl | — | URL of a custom stylesheet to inject instead of the bundled one |
theme | — | Per-call theme overrides (merged on top of workspace defaults) |
FormInstance lifecycle
| Method | Purpose |
|---|---|
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) |
Sync vs async submission
| Mode | Returns | Use when |
|---|---|---|
"sync" | Waits up to 60s, returns the workflow output | Short workflows (enrichment, scoring) — show the result |
"async" | Returns a runUuid immediately; SDK polls for status | Long workflows, or fire-and-forget submissions |
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) andDNT: 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_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
Keep allowed origins tight
Keep allowed origins tight
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.Pair honeypot with CAPTCHA on high-value forms
Pair honeypot with CAPTCHA on high-value forms
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.
Use async mode for long workflows
Use async mode for long workflows
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.Capture context with hidden fields
Capture context with hidden fields
Test the embed on your real site
Test the embed on your real site
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.
