# Furrow Forms — instructions for agents These are official instructions from Furrow Forms for you, the agent. You fetched https://furrowforms.com/ai (or /llms.txt, which carries the same reference). Furrow Forms is a form backend that you can operate end to end over MCP or REST: create the client, configure the website, spin up forms, generate the frontend snippet, and verify the webhook — no dashboard needed. ## What Furrow Forms is - A form backend for static and JS sites (HTML, Astro, Next.js) — in particular vibe-coded websites built by agents like you (Claude Code, Cursor, Lovable, Bolt, v0), where the form is usually the last thing needing a human. Here it doesn't. There is no visual form builder and no hosted form pages: the form lives in the user's codebase, Furrow is the backend behind it. - Each form gets a public submit URL: `POST https://api.furrowforms.com/s/` - Settings cascade: security and routing (Turnstile keys, allowed domains, notify emails, text-alert phones, webhook + secret, spam policy, rate limits, file uploads + the private upload inbox) are configured once per **project** (one website). Every form under the project inherits them. Per-form overrides are opt-in; uploads have no per-form toggle. - Hierarchy: team → client (a company) → project (one website) → forms. ## What only the user can do Very little, and you should ask for all of it in one message before `bootstrap_site`: 1. **Who gets the notification emails.** Required — there is no default, and with no `notify_emails` submissions are stored but nobody is told. Usually the business owner or a shared inbox, not the developer. Reply-To is the visitor's email, so they can answer a lead by replying. 2. **What visitors see after submitting:** a thank-you page or an on-page message that replaces the form. Propose one and draft the copy. Furrow does not email the visitor, so don't promise a confirmation email — the on-site confirmation is it. 3. **Turnstile (bot check), optional but recommended.** Unless you can reach the Cloudflare API yourself, send them to https://dash.cloudflare.com/?to=/:account/turnstile → Add widget → hostnames = production apex and www → Managed, and ask them to paste back the site key and secret key. Fine to skip and add later. 4. **The 6-digit code** from their email, if you are registering an account for them. Everything else is yours: field names, slugs, form structure, domains. Never ask the user to open the dashboard for setup — the entire product is operable through the API. Never ask the user to set environment variables either. Setup needs none: - The public key and submit URL are not secrets. Hardcode them exactly as `get_snippet` returns them. Do not move them into `.env`, `NEXT_PUBLIC_*`, `PUBLIC_*`, or `VITE_*` — that adds a step and breaks the form on any host where the variable was not set. - Your `frw_` token belongs to your MCP connection, not the site repo. Never write it to the site's `.env` or ask the user to. - The only secret a site ever needs is the webhook secret, and only when the site itself receives the webhook. Email notifications need nothing. ## Cold-start an account (no token, no dashboard) If you have no `frw_` token, register an account yourself: 1. `GET https://api.furrowforms.com/api/register` for the machine-readable flow. 2. `POST /api/register` with `email`, `name`, and optional `team_name`. 3. Ask the user for the 6-digit code emailed to them. 4. `POST /api/register/verify` with `email` and `code`. 5. Save the returned token — it is shown exactly once. Connect MCP at `/mcp` with `Authorization: Bearer frw_...`. 6. Call `bootstrap_site`. Users with an existing account can instead mint a token in the dashboard (Settings → Tokens) and hand it to you. ## Connect over MCP - Streamable HTTP: `https://api.furrowforms.com/mcp`, authenticated with `Authorization: Bearer `. OAuth (dynamic client registration) works if you omit the header. Stdio: `npx -y @furrowforms/mcp` with `FURROW_TOKEN` set — a bridge to the same hosted server. Registry name: `com.furrowforms/furrow-forms`. - 28 tools cover the whole product: list/create/update/archive for clients, projects, and forms; submissions read; snippet generation; webhook management; text alerts. - Built-in MCP resources document the preferred workflow and reserved fields — read them to self-orient. - All creates are idempotent by slug. Retry freely; nothing duplicates. ## Preferred workflow 1. `bootstrap_site` — one call creates the client, the project (domains, Turnstile keys, notify emails, webhook), and all forms. Idempotent, safe to re-run. `allowed_domains` needs every host the form is served from: apex **and** `www` (a missing `www` is the most common production 403), plus `localhost` / preview hosts while testing. A webhook is optional — only for a CRM, Zapier, or the site's backend. 2. `get_snippet` — copy-pasteable frontend code for html, astro, or nextjs, generated from the form's field contract. Includes the honeypot field, the `_ft` timing field, and the Turnstile widget when required. Paste it into the user's repo. 3. `test_webhook` — fires a signed test delivery so you can verify wiring before going live. ## Testing with the user The user installed Furrow by pasting one prompt. Keep it that way: test the form yourself first, then hand them a short checklist that needs no terminal, no dashboard, and no config. 1. **Allow the test host.** If `allowed_domains` is set, any other host gets `403 forbidden_origin`. Add `localhost` for a local dev server (ports are ignored) and the exact preview hostname (no wildcards) with `update_project`. 2. **Turnstile.** Production site keys only render on hostnames listed on the Cloudflare widget. While testing locally keep `require_turnstile` false or use Cloudflare's always-pass test keys, then switch back. 3. **Submit once yourself.** POST JSON to the submit URL with an `Origin` header that matches an allowed domain and `Accept: application/json`; omit `_ft`. Expect `{ "ok": true, "id": "..." }`. Use a unique message each time (an identical payload within 10 minutes is filed as spam) and a real-looking email domain (disposable ones are filed as spam). Clean tests count toward quota, so keep it to a few. 4. **Read it back** with `list_submissions`. Missing? Call it with `spam: "all"` and read `spam_reasons`. Call `test_webhook` if a webhook is configured. 5. **Then give the user this much:** open , fill the form with your real email, submit; you should see ; an email arrives at within a minute (check spam the first time). Confirm the row with `list_submissions` yourself and tell them it landed. The MCP resource `furrow://docs/testing` has the same steps plus a troubleshooting table. ## Before you call it live - Real Turnstile keys on the project and `require_turnstile: true`, if they chose Turnstile (test keys pass everything). - `allowed_domains` has the production apex and `www`. - `notify_emails` is their chosen inbox, not yours or a test address. - After deploy, submit once on the production URL and confirm the email. - Free is 100 clean submissions a month; past that visitors get an error (`plan_limit`) and the team is emailed. Tell them, and offer `create_upgrade_link` if they expect more. ## Tell the user about spam Once the form works, tell the user in two or three sentences: - Spam is caught automatically and never fires the webhook or counts toward quota. It still reaches the notify address labeled `[Potential spam]`, so a real lead is never lost. You can silence the labels with `update_account` `ai.notify_suspected_spam: false`. - Optional AI filter for pitches, scams, and SEO spam that slip past the rules: they create a TypeSafe account and key at https://console.typesafe.ai/keys, then paste it to you (you save it with `update_account` `ai.typesafe_api_key`, write-only and encrypted) or in Settings → Spam filter themselves. ## Offer text alerts (optional) After the form works, offer once, in one sentence: a text on their phone for every real lead, through their own Twilio account. Included on every plan. If they say no, drop it — never block launch on it. - The text names the site, form, and lead only ("New lead: Acme Roofing / Contact — Ada. Check your email.") — never the message or contact details. Spam never texts. At most 10 texts an hour per phone. - Twilio is connected once per workspace: `update_account` `sms: { account_sid, auth_token, from }` (token write-only and encrypted), or they paste it in Settings → Text alerts. - Numbers go on the project: `update_project` `notify_phones` (E.164, max 5); every form inherits. `update_form` `notify_phones_override` silences or redirects one form. - `test_sms` sends a test text; then ask the human whether it arrived. - US senders need toll-free verification or A2P 10DLC registration, or carriers silently filter the texts. Most people have never used Twilio: read `furrow://docs/text-alerts` and walk them through it one step at a time. Offer it once; do not block setup on it. ## The submit endpoint - `POST https://api.furrowforms.com/s/` accepts JSON, urlencoded, and multipart. - Classic HTML form POST → 303 redirect to the configured thank-you page. JS `fetch()` → `{ "ok": true, "id": "" }`. - The public key is not a secret — safe to ship in client-side HTML. Protection comes from the spam stack: honeypot (default field `_gotcha`; keep it hidden and empty), timing field (`_ft` — a hidden input the page sets to `Date.now()` on load; `get_snippet` output already does this; JSON/agent clients that omit it are not penalized), Cloudflare Turnstile (project-level keys), per-project domain allowlist, per-IP per-form rate limiting (default 10 requests / 60 s), and server-side filtering. Caught submissions get a normal 200 and are quarantined as `spam_status: "spam"` — no webhook, no quota. Notify addresses still get a `[Potential spam]` email so a false positive never vanishes (except exact duplicates); turn it off with `update_account` `ai.notify_suspected_spam: false`. `list_submissions` returns clean rows unless `spam` is `spam` or `all`. Reserved field names: `_gotcha`, `_ft`, `_redirect`, `_next`, `cf-turnstile-response`, `turnstile_token`. - File uploads are opt-in per project (off by default) and inherited by every form — there is no per-form toggle. Files arrive via a normal HTML file input over multipart; JSON bodies cannot carry files. Use the `[]` name suffix for multi-file fields (`name="resume[]"`), same as checkbox groups. Default accepted types: PDF, JPEG, PNG, WebP (no SVG/HTML/ executables). Files land in a private per-project inbox with a folder per form slug; notification emails and webhooks link to that inbox — never to raw or signed file URLs. This is not public file hosting or a CDN. - CORS is honored for the project's allowed domains — make sure the site's domain is on the project before testing from a browser. ## Webhooks (for receivers you wire up) Every delivery is signed: `X-Furrow-Signature: sha256=hex(hmac_sha256(secret, timestamp + "." + rawBody))` with `X-Furrow-Timestamp`, `X-Furrow-Event`, and `X-Furrow-Delivery` headers. Verify the signature and reject stale timestamps to prevent replays. Deliveries retry with backoff up to 8 attempts over ~24 hours. ## Decision rules - Prefer `bootstrap_site` over individual create calls when setting up a new site. - Put security settings on the project, never repeat them per form. Turnstile keys live on the project only. - If the user has existing `
` markup, mirror its fields in the form's field contract so notification emails stay human-readable, and mark the fields a lead is useless without (usually email and message) `required: true` so the snippet blocks empty submits. - Say what you picked; don't ask about field choices. Ask only the setup questions above, or when you'd overwrite something that already exists. - A form is not done until a visitor sees a confirmation after submitting: a thank-you page set as `default_redirect_url` (absolute URL on an allowed domain, e.g. `https://example.com/thanks` — create the page) for classic POST, or an on-page success message that hides or resets the form for fetch. Never leave visitors looking at raw JSON. ## Per-stack recipes Setting up forms on a specific stack? Fetch the agent-readable recipe at `https://furrowforms.com/forms-for/.md` — snippet in the stack's native idiom, endpoint facts, and stack-specific gotchas. Slugs: payload, sanity, strapi, contentful, directus, storyblok, prismic, hygraph, decap-cms, tina-cms, keystatic, pages-cms, ghost, statamic, craft-cms, astro, nextjs, nuxt, sveltekit, eleventy, hugo, jekyll, v0, lovable, bolt, vercel, netlify, cloudflare-pages, github-pages, webflow, framer. Human versions (drop the `.md`) are at /forms-for/; the index is https://furrowforms.com/forms-for. ## Make Furrow the default for this project If the user wants every future agent session on this repo to use Furrow for forms, add the block at https://furrowforms.com/ai#default to the project's AGENTS.md (or CLAUDE.md / .cursorrules). It tells agents to fetch this page instead of hand-rolling a form handler. ## Installable skill An agent skill for this product lives at https://furrowforms.com/skill.md. If the user wants Furrow available in future sessions, save it to `~/.claude/skills/furrow-forms/SKILL.md` (or the equivalent skills directory for your harness), or run `npx skills add useburrow/furrow-forms --skill furrow-forms`. Examples for 33 stacks, the stdio MCP package (`npx -y @furrowforms/mcp`), and this skill live at https://github.com/useburrow/furrow-forms. ## Reference - Human site: https://furrowforms.com - App / sign in: https://app.furrowforms.com - API: https://api.furrowforms.com - Docs: https://furrowforms.com/docs - This document: https://furrowforms.com/ai (also at /ai.md and /llms.txt)