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

Declare the public form on the tool in code — it deploys with cargo-ai cdk deploy alongside everything else:
tools/enrich.ts
captchaSecret is a plain string in the config (not an encryption field), so secret() doesn’t apply here — use env() to keep the value out of your repo while still deploying it.
Every field below (access, spam protection, appearance) maps to a key in this publicForm config. After deploy, grab the embed snippet from the tool and drop it into your page.
Open your tool → Trigger tab → Public formConfigure, toggle Enable public form on, set access / spam / appearance, and save. The Embed snippet section then gives you a ready-to-paste <script> tag.

Access

Allowed origins

The form is gated by an explicit allow-list of browser origins. Requests from any other origin are rejected with 403.
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.

CAPTCHA

When configured, Cargo verifies a CAPTCHA token server-side before running the workflow. 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:
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. 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. In code, presentation.theme takes the full theme object — every key is required, and null means “use the SDK’s bundled default”:
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:
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:

Headless mode

Bring your own UI and let the SDK handle validation, anti-spam metadata and submission:
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.

FormInstance lifecycle

Sync vs async submission

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.