foxform_create_form
Create a new form, including per-screen conditional logic. Requires a WRITE-scoped API key.
Args:
- title (string): form title (required)
- description (string, optional)
- theme (string, optional): one of amber|base|midnight|ocean|sunset|forest|lavender|minimal (default amber, the copper-to-amber FoxForm theme)
- questions (array, optional): array of screen objects ({ id, type, title, required, variableName?, choices?, logic?, ... }); omit to start empty
- thank_you_message (string, optional)
- format ('single_question' | 'single_page' | 'chatbot', optional): how the form is presented (see below)
- sections (array, optional): single_page page groups, [{ id }]
- chatbot_skin ('instagram' | 'whatsapp', optional): chatbot chrome
- chatbot_profile (object, optional): chatbot header profile ({ name, handle, followers_text, phone, status, verified, avatar_url })
Returns: { form } with the created form (including its id and slug). The form starts as a draft — call foxform_publish_form to make it live.
Screen fields are validated: unknown fields are REJECTED instead of being stored and ignored, and branching rules are cross-checked against the screen ids in the same payload.
Form format (DEVF-296 / DEVF-322): pass `format` to choose how the form is presented.
- 'single_question' (default): one screen at a time (classic).
- 'single_page': every screen on one scrollable page. Optionally group screens into pages with `sections`: `[{ id }]` in order (the 'Section N' label comes from position; no editable title).
- 'chatbot': an Instagram/WhatsApp DM-style conversation. Set `chatbot_skin` ('instagram' | 'whatsapp', default instagram) and `chatbot_profile` (all fields optional, each with a fallback). The question/branching engine is unchanged — only the presentation differs.
These fields require the account's form-format feature to be enabled (live in production); when off the API ignores them and the form stays single_question.
End screen ('end'): title + description, an optional button (buttonEnabled false hides it; buttonText, buttonLink, buttonOpenNewTab) and an optional automatic redirect after submit: autoRedirect true + autoRedirectUrl (absolute http/https URL; the form link's query params such as utm_* are passed along and {{param}} tokens are filled from them) + autoRedirectDelay (seconds 0-10, default 3). The response is recorded before the redirect; respondents see a "Redirecting in N s" line with a "Go now" link.
CONDITIONAL LOGIC (branching), per screen — stored in `questions[].logic`:
logic.conditionalNavigationV2 = {
enabled: true,
groups: [ // groups are OR-joined; FIRST matching group wins
{
id: "grp-1",
conditions: [ // conditions inside a group are AND-joined
{ id: "cond-1", left: "{{quer_testar}}", operator: "equal_to", right: "Ainda não" }
],
then: { type: "specific_screen", targetScreenId: "s-motivos" }
}
]
}
- `then.type`: 'next_screen' | 'previous_screen' | 'specific_screen' (needs targetScreenId = another screen's `id`) | 'end_form'.
Add `then.url` (+ optional `openNewTab`) to redirect to an external URL instead.
- `operator`: 'equal_to' | 'not_equal_to' | 'greater_than' | 'greater_or_equal_than' | 'less_than' | 'less_or_equal_than' | 'contains'.
- `left`/`right` are EXPRESSION strings: a literal ("10", "Ainda não"), a variable ("{{score}}", "{{minha_var}}" = the screen's `variableName`), or arithmetic ("calc({{peso}}/(({{altura}}/100)*({{altura}}/100)))").
- Comparing an ANSWER: use `left: "{{<variableName of the deciding screen>}}"` and `right` = the option's `label` OR its `value` (both match).
- `{{score}}` is the running sum of `points` on the options picked so far (`choices[].points`, `images[].points`) — that is how score-based branching works.
- A navigation group with no conditions NEVER matches. `enabled: false` stores the rules but disables them.
- Screen-level conditional display uses the same group shape: `logic.display = { enabled: true, groups: [...], showAfterSeconds?: n }` (`then` is ignored — THEN means "show").
- Other logic keys: `logic.autoAdvance = { enabled, delaySeconds? }`, `logic.navigationBehavior = { onButtonClick?, onAutoAdvance?, targetScreenId? }`.
- `logic.conditionalNavigation` (legacy, pre-DEVF-161) is still read and migrated on load — don't author new rules with it.
Unknown fields are REJECTED (they used to be stored and silently ignored): `logic` as an array, or `rules`/`branching`/`conditions`/`goto`/`jump`/`nextScreen` anywhere, are not read by any renderer.
foxform_get_form
Fetch a single form by ID, including its full question list, per-screen conditional logic and settings.
Args:
- form_id (string): the form's ID (from foxform_list_forms)
- response_format ('markdown' | 'json')
Returns { form, public_url }: the full form object (id, title, description, slug, status, theme, questions[], thank_you_message, timestamps, plus `format` and — when set — `sections`, `chatbot_skin`, `chatbot_profile`) plus `public_url` — the public link (`https://forms.foxform.app/<slug>`), which is live only when status is 'published'. Use `public_url` verbatim; never build the URL yourself (the public domain is forms.foxform.app, not foxform.app).
Each screen in `questions[]` carries its own `logic` (branching / conditional display) and, for choice screens, `choices[]`/`images[]` with their `points` and `value`. The markdown output summarises every rule; use response_format 'json' to get the exact stored objects (that's the shape foxform_update_form expects back).
Form format (DEVF-296 / DEVF-322): pass `format` to choose how the form is presented.
- 'single_question' (default): one screen at a time (classic).
- 'single_page': every screen on one scrollable page. Optionally group screens into pages with `sections`: `[{ id }]` in order (the 'Section N' label comes from position; no editable title).
- 'chatbot': an Instagram/WhatsApp DM-style conversation. Set `chatbot_skin` ('instagram' | 'whatsapp', default instagram) and `chatbot_profile` (all fields optional, each with a fallback). The question/branching engine is unchanged — only the presentation differs.
These fields require the account's form-format feature to be enabled (live in production); when off the API ignores them and the form stays single_question.
End screen ('end'): title + description, an optional button (buttonEnabled false hides it; buttonText, buttonLink, buttonOpenNewTab) and an optional automatic redirect after submit: autoRedirect true + autoRedirectUrl (absolute http/https URL; the form link's query params such as utm_* are passed along and {{param}} tokens are filled from them) + autoRedirectDelay (seconds 0-10, default 3). The response is recorded before the redirect; respondents see a "Redirecting in N s" line with a "Go now" link.
CONDITIONAL LOGIC (branching), per screen — stored in `questions[].logic`:
logic.conditionalNavigationV2 = {
enabled: true,
groups: [ // groups are OR-joined; FIRST matching group wins
{
id: "grp-1",
conditions: [ // conditions inside a group are AND-joined
{ id: "cond-1", left: "{{quer_testar}}", operator: "equal_to", right: "Ainda não" }
],
then: { type: "specific_screen", targetScreenId: "s-motivos" }
}
]
}
- `then.type`: 'next_screen' | 'previous_screen' | 'specific_screen' (needs targetScreenId = another screen's `id`) | 'end_form'.
Add `then.url` (+ optional `openNewTab`) to redirect to an external URL instead.
- `operator`: 'equal_to' | 'not_equal_to' | 'greater_than' | 'greater_or_equal_than' | 'less_than' | 'less_or_equal_than' | 'contains'.
- `left`/`right` are EXPRESSION strings: a literal ("10", "Ainda não"), a variable ("{{score}}", "{{minha_var}}" = the screen's `variableName`), or arithmetic ("calc({{peso}}/(({{altura}}/100)*({{altura}}/100)))").
- Comparing an ANSWER: use `left: "{{<variableName of the deciding screen>}}"` and `right` = the option's `label` OR its `value` (both match).
- `{{score}}` is the running sum of `points` on the options picked so far (`choices[].points`, `images[].points`) — that is how score-based branching works.
- A navigation group with no conditions NEVER matches. `enabled: false` stores the rules but disables them.
- Screen-level conditional display uses the same group shape: `logic.display = { enabled: true, groups: [...], showAfterSeconds?: n }` (`then` is ignored — THEN means "show").
- Other logic keys: `logic.autoAdvance = { enabled, delaySeconds? }`, `logic.navigationBehavior = { onButtonClick?, onAutoAdvance?, targetScreenId? }`.
- `logic.conditionalNavigation` (legacy, pre-DEVF-161) is still read and migrated on load — don't author new rules with it.
Unknown fields are REJECTED (they used to be stored and silently ignored): `logic` as an array, or `rules`/`branching`/`conditions`/`goto`/`jump`/`nextScreen` anywhere, are not read by any renderer.
foxform_update_form
Update an existing form's fields, including each screen's conditional logic (branching). Requires a WRITE-scoped API key. Only the fields you pass are changed.
Args:
- form_id (string): the form to update (required)
- title (string, optional)
- description (string, optional)
- theme (string, optional): one of amber|base|midnight|ocean|sunset|forest|lavender|minimal
- questions (array, optional): replaces the FULL screen list — there is no per-screen patch. To add logic to one screen: call foxform_get_form with response_format 'json', edit that screen's `logic`, and send the whole array back.
- thank_you_message (string, optional)
- format ('single_question' | 'single_page' | 'chatbot', optional)
- sections (array, optional): single_page page groups, [{ id }]
- chatbot_skin ('instagram' | 'whatsapp', optional)
- chatbot_profile (object, optional): chatbot header profile ({ name, handle, followers_text, phone, status, verified, avatar_url })
Returns: { form } with the updated form.
Screen fields are validated: unknown fields are REJECTED instead of being stored and ignored (the API accepts arbitrary keys but no renderer reads them), `then.targetScreenId` must be the id of a screen in the same payload, and `{{variables}}` that no screen exposes come back as warnings.
Form format (DEVF-296 / DEVF-322): pass `format` to choose how the form is presented.
- 'single_question' (default): one screen at a time (classic).
- 'single_page': every screen on one scrollable page. Optionally group screens into pages with `sections`: `[{ id }]` in order (the 'Section N' label comes from position; no editable title).
- 'chatbot': an Instagram/WhatsApp DM-style conversation. Set `chatbot_skin` ('instagram' | 'whatsapp', default instagram) and `chatbot_profile` (all fields optional, each with a fallback). The question/branching engine is unchanged — only the presentation differs.
These fields require the account's form-format feature to be enabled (live in production); when off the API ignores them and the form stays single_question.
End screen ('end'): title + description, an optional button (buttonEnabled false hides it; buttonText, buttonLink, buttonOpenNewTab) and an optional automatic redirect after submit: autoRedirect true + autoRedirectUrl (absolute http/https URL; the form link's query params such as utm_* are passed along and {{param}} tokens are filled from them) + autoRedirectDelay (seconds 0-10, default 3). The response is recorded before the redirect; respondents see a "Redirecting in N s" line with a "Go now" link.
CONDITIONAL LOGIC (branching), per screen — stored in `questions[].logic`:
logic.conditionalNavigationV2 = {
enabled: true,
groups: [ // groups are OR-joined; FIRST matching group wins
{
id: "grp-1",
conditions: [ // conditions inside a group are AND-joined
{ id: "cond-1", left: "{{quer_testar}}", operator: "equal_to", right: "Ainda não" }
],
then: { type: "specific_screen", targetScreenId: "s-motivos" }
}
]
}
- `then.type`: 'next_screen' | 'previous_screen' | 'specific_screen' (needs targetScreenId = another screen's `id`) | 'end_form'.
Add `then.url` (+ optional `openNewTab`) to redirect to an external URL instead.
- `operator`: 'equal_to' | 'not_equal_to' | 'greater_than' | 'greater_or_equal_than' | 'less_than' | 'less_or_equal_than' | 'contains'.
- `left`/`right` are EXPRESSION strings: a literal ("10", "Ainda não"), a variable ("{{score}}", "{{minha_var}}" = the screen's `variableName`), or arithmetic ("calc({{peso}}/(({{altura}}/100)*({{altura}}/100)))").
- Comparing an ANSWER: use `left: "{{<variableName of the deciding screen>}}"` and `right` = the option's `label` OR its `value` (both match).
- `{{score}}` is the running sum of `points` on the options picked so far (`choices[].points`, `images[].points`) — that is how score-based branching works.
- A navigation group with no conditions NEVER matches. `enabled: false` stores the rules but disables them.
- Screen-level conditional display uses the same group shape: `logic.display = { enabled: true, groups: [...], showAfterSeconds?: n }` (`then` is ignored — THEN means "show").
- Other logic keys: `logic.autoAdvance = { enabled, delaySeconds? }`, `logic.navigationBehavior = { onButtonClick?, onAutoAdvance?, targetScreenId? }`.
- `logic.conditionalNavigation` (legacy, pre-DEVF-161) is still read and migrated on load — don't author new rules with it.
Unknown fields are REJECTED (they used to be stored and silently ignored): `logic` as an array, or `rules`/`branching`/`conditions`/`goto`/`jump`/`nextScreen` anywhere, are not read by any renderer.