Skill formloop
formloop lets you generate a Typeform-quality form on-the-fly, ask a human structured questions, and resume execution when the form is submitted. The missing layer when AskUserQuestion saturates and chat is the wrong UI.
100% local. Server runs on 127.0.0.1:3847. No accounts, no cloud, no telemetry.
When to use formloop (vs alternatives)
| Situation |
Use |
| 1-3 questions, 2-4 textual options, 1 respondent |
Built-in AskUserQuestion |
| 4+ questions, 1 respondent |
formloop ephemeral (persistent: false) |
| 4+ questions, multiple respondents (team, panel, audit) |
formloop persistent + list |
| Visual choice — mockups, layouts, palettes |
formloop html-pick |
| Rank by preference |
formloop rank (drag-and-drop) |
| Numeric value with live visual preview |
formloop scale-preview |
| Respondent fills from their phone |
formloop (mobile-first) |
Bootstrap (once per session)
Check that the server is up, otherwise start it. npx formloop boots a Next.js server on 127.0.0.1:3847 and auto-builds on first run.
# Verify
curl -s -m 2 http://127.0.0.1:3847/ -o /dev/null && echo "up" || echo "down"
# Start (one-line, no deps to install — the bin does it)
npx formloop
In Claude Code: run npx formloop via Bash run_in_background: true, then poll for readiness with until curl -s -m 2 http://127.0.0.1:3847/ -o /dev/null; do sleep 1; done.
Ephemeral mode (1 respondent, urgent decision)
# 1. Create the form (auto-opens in browser unless --no-open)
npx formloop create --spec '{
"title": "Hero design direction",
"blocks": [
{"kind": "html-pick", "id": "design", "title": "Which direction?",
"options": [
{"id": "a", "label": "Minimal", "html": "<div style=\"...\">Mockup A</div>"},
{"id": "b", "label": "Bold", "html": "<div style=\"...\">Mockup B</div>"}
]},
{"kind": "textarea", "id": "notes", "title": "Any notes?", "required": false}
]
}'
# 2. Wait for submission (run in background so the conversation auto-resumes)
npx formloop wait --form-id <ID> --timeout 1800
Output of wait:
{
"form_id": "ABC123",
"answers": {
"Which direction?": "b",
"Any notes?": "Looks great"
},
"submitted_at": "2026-05-26T15:00:00.000Z"
}
Persistent mode (N respondents — audit, panel, survey)
npx formloop create --spec '{
"title": "SAV rules audit — Batch 1",
"persistent": true,
"respondentField": {
"type": "email-name",
"required": true,
"intro": "Your first name and email, so we can compare answers across reviewers"
},
"blocks": [
{"kind": "html",
"html": "<h2>Why this form?</h2><p>We audit ~80 SAV rules to identify critical / secondary / drop. Your input compared to others shows where we agree and where we don'''t.</p>"},
{"kind": "html",
"html": "<div style=\"padding:16px;background:#f4f4f8;border-radius:8px\"><strong>Rule 1 — Dates</strong><br><br>NEVER give a past date as future date.<br>❌ \"shipping 21/04\" when we'''re 25/04<br>✅ \"shipping within 2-3 business days\"</div>"},
{"kind": "mc", "id": "rule1_decision", "title": "Rule 1 — Action?",
"options": ["Keep as-is", "Amend", "Drop"]},
{"kind": "textarea", "id": "rule1_comment", "title": "Rule 1 — Notes",
"required": false},
{"kind": "mc", "id": "rule1_importance", "title": "Rule 1 — Importance",
"options": ["Critical", "Important", "Secondary", "Not sure"]}
]
}'
Then:
# Share the same URL with N respondents (email/Slack/etc).
# List all submissions at any time:
npx formloop list --form-id ABC123
# Or block until N submissions are in:
# (not yet in the bin — use the Python SDK: ask_form.py wait-n)
Block types
kind |
Purpose |
Required fields |
Optional |
html |
Context / mockup / separator — not a question |
html |
— |
text |
Short text (1 line) |
id, title |
placeholder, required |
textarea |
Long text |
id, title |
placeholder, required |
mc |
Single choice (radio) |
id, title, options[] |
description, required |
multi |
Multi choice (checkboxes) |
id, title, options[] |
idem |
yn |
Yes/No |
id, title |
labels: {yes, no}, required |
number |
Integer or decimal |
id, title |
min, max, required |
scale |
1-N numbered scale |
id, title, min, max |
minLabel, maxLabel, required |
html-pick |
Clickable HTML mockup grid |
id, title, options: {id, html, label?}[] |
multi, required |
rank |
Drag-and-drop ordering |
id, title, options[] |
required |
scale-preview |
Slider with live HTML preview (placeholders {{value}} and {{percent}}) |
id, title, min, max, previewHtml |
minLabel, maxLabel, required |
All embedded HTML is sanitized via DOMPurify server-side. <script>, event handlers, and iframes are stripped. SVG is allowed (charts, illustrations).
respondentField — capture identity (persistent only)
"respondentField": {
"type": "email-name",
"required": true,
"intro": "Your first name and email"
}
Effect: Formloop renders 2 fields (first name + email) at the very start of the form. Each submission then includes respondent: {name, email} alongside answers.
Best practices
For multi-respondent (audit, panel):
persistent: true is mandatory — otherwise only the first respondent counts.
respondentField: email-name unless you really want anonymous.
- Keep under 30 blocks. Beyond, drop-off explodes. Split big audits across thematic forms.
- Frame each item to audit with an
html block that renders the item verbatim (title + ✅/❌ + real example) before the decision questions. Respondents shouldn't need to scroll a .md side-by-side.
- For diffusion: email/Slack with a clear intro — why this form, estimated time, how results are used.
For ephemeral (1 decision):
- Cap at 6-8 blocks.
- Prefer
html-pick over mc when the choice is visual.
- Use
html blocks as intros, not description, when you want rich HTML.
For both:
- Explicit titles: "Hero design direction" beats "A few questions".
required: false on free-form textarea comments.
- Test the form URL yourself before sending it.
Limitations
- No conditional logic (skip-if/else). All questions are seen by all respondents.
- No mid-form computation. Aggregate after submission.
- HTML sanitization is strict. Use
scale-preview for interactivity.
- Spec TTL 24h, ephemeral response TTL 1h. Restart of the local server wipes everything (in-memory store).
- No push notifications to respondents — sending email/Slack is on the caller.
- 1 form = 1 spec. To iterate, create a new form (the old one stays accessible).
Self-hosting
By default, npx formloop runs entirely on 127.0.0.1:3847 with an in-memory store. If you want persistence across restarts, set KV_REST_API_URL + KV_REST_API_TOKEN to point at an Upstash Redis (or any Upstash-compatible KV).
To require API auth (multi-user shared server), set WEBHOOK_SECRET=<long-random-string> and pass it as the x-webhook-secret header on every /api/forms* call.
1---2name: formloop3description: Create Typeform-quality forms on-the-fly to ask a human structured questions, then resume execution when the form is submitted. Supports rich blocks (HTML mockup grids, drag-and-drop ranks, sliders with live previews) that chat and AskUserQuestion can't render. Use when you need 4+ structured inputs from a human in the loop, or when the choice is visual (designs, mockups, layouts).4---5
6# Skill formloop
7
8**formloop** lets you generate a Typeform-quality form on-the-fly, ask a human structured questions, and resume execution when the form is submitted. The missing layer when `AskUserQuestion` saturates and chat is the wrong UI.
9
10100% local. Server runs on `127.0.0.1:3847`. No accounts, no cloud, no telemetry.
11
12## When to use formloop (vs alternatives)
13
14| Situation | Use |
15|---|---|
16| 1-3 questions, 2-4 textual options, 1 respondent | Built-in `AskUserQuestion` |
17| 4+ questions, 1 respondent | formloop ephemeral (`persistent: false`) |
18| 4+ questions, multiple respondents (team, panel, audit) | formloop persistent + `list` |
19| Visual choice — mockups, layouts, palettes | formloop `html-pick` |
20| Rank by preference | formloop `rank` (drag-and-drop) |
21| Numeric value with live visual preview | formloop `scale-preview` |
22| Respondent fills from their phone | formloop (mobile-first) |
23
24## Bootstrap (once per session)
25
26Check that the server is up, otherwise start it. `npx formloop` boots a Next.js server on `127.0.0.1:3847` and auto-builds on first run.
27
28```bash
29# Verify
30curl -s -m 2 http://127.0.0.1:3847/ -o /dev/null && echo "up" || echo "down"
31
32# Start (one-line, no deps to install — the bin does it)
33npx formloop
34```
35
36In Claude Code: run `npx formloop` via `Bash run_in_background: true`, then poll for readiness with `until curl -s -m 2 http://127.0.0.1:3847/ -o /dev/null; do sleep 1; done`.
37
38## Ephemeral mode (1 respondent, urgent decision)
39
40```bash
41# 1. Create the form (auto-opens in browser unless --no-open)
42npx formloop create --spec '{
43 "title": "Hero design direction",
44 "blocks": [
45 {"kind": "html-pick", "id": "design", "title": "Which direction?",
46 "options": [
47 {"id": "a", "label": "Minimal", "html": "<div style=\"...\">Mockup A</div>"},
48 {"id": "b", "label": "Bold", "html": "<div style=\"...\">Mockup B</div>"}
49 ]},
50 {"kind": "textarea", "id": "notes", "title": "Any notes?", "required": false}
51 ]
52}'
53
54# 2. Wait for submission (run in background so the conversation auto-resumes)
55npx formloop wait --form-id <ID> --timeout 1800
56```
57
58Output of `wait`:
59```json
60{
61 "form_id": "ABC123",
62 "answers": {
63 "Which direction?": "b",
64 "Any notes?": "Looks great"
65 },
66 "submitted_at": "2026-05-26T15:00:00.000Z"
67}
68```
69
70## Persistent mode (N respondents — audit, panel, survey)
71
72```bash
73npx formloop create --spec '{
74 "title": "SAV rules audit — Batch 1",
75 "persistent": true,
76 "respondentField": {
77 "type": "email-name",
78 "required": true,
79 "intro": "Your first name and email, so we can compare answers across reviewers"
80 },
81 "blocks": [
82 {"kind": "html",
83 "html": "<h2>Why this form?</h2><p>We audit ~80 SAV rules to identify critical / secondary / drop. Your input compared to others shows where we agree and where we don'''t.</p>"},
84 {"kind": "html",
85 "html": "<div style=\"padding:16px;background:#f4f4f8;border-radius:8px\"><strong>Rule 1 — Dates</strong><br><br>NEVER give a past date as future date.<br>❌ \"shipping 21/04\" when we'''re 25/04<br>✅ \"shipping within 2-3 business days\"</div>"},
86 {"kind": "mc", "id": "rule1_decision", "title": "Rule 1 — Action?",
87 "options": ["Keep as-is", "Amend", "Drop"]},
88 {"kind": "textarea", "id": "rule1_comment", "title": "Rule 1 — Notes",
89 "required": false},
90 {"kind": "mc", "id": "rule1_importance", "title": "Rule 1 — Importance",
91 "options": ["Critical", "Important", "Secondary", "Not sure"]}
92 ]
93}'
94```
95
96Then:
97
98```bash
99# Share the same URL with N respondents (email/Slack/etc).
100# List all submissions at any time:
101npx formloop list --form-id ABC123
102
103# Or block until N submissions are in:
104# (not yet in the bin — use the Python SDK: ask_form.py wait-n)
105```
106
107## Block types
108
109| `kind` | Purpose | Required fields | Optional |
110|---|---|---|---|
111| `html` | Context / mockup / separator — not a question | `html` | — |
112| `text` | Short text (1 line) | `id`, `title` | `placeholder`, `required` |
113| `textarea` | Long text | `id`, `title` | `placeholder`, `required` |
114| `mc` | Single choice (radio) | `id`, `title`, `options[]` | `description`, `required` |
115| `multi` | Multi choice (checkboxes) | `id`, `title`, `options[]` | idem |
116| `yn` | Yes/No | `id`, `title` | `labels: {yes, no}`, `required` |
117| `number` | Integer or decimal | `id`, `title` | `min`, `max`, `required` |
118| `scale` | 1-N numbered scale | `id`, `title`, `min`, `max` | `minLabel`, `maxLabel`, `required` |
119| **`html-pick`** | **Clickable HTML mockup grid** | `id`, `title`, `options: {id, html, label?}[]` | `multi`, `required` |
120| **`rank`** | **Drag-and-drop ordering** | `id`, `title`, `options[]` | `required` |
121| **`scale-preview`** | **Slider with live HTML preview** (placeholders `{{value}}` and `{{percent}}`) | `id`, `title`, `min`, `max`, `previewHtml` | `minLabel`, `maxLabel`, `required` |
122
123All embedded HTML is sanitized via DOMPurify server-side. `<script>`, event handlers, and iframes are stripped. SVG is allowed (charts, illustrations).
124
125## `respondentField` — capture identity (persistent only)
126
127```json
128"respondentField": {
129 "type": "email-name",
130 "required": true,
131 "intro": "Your first name and email"
132}
133```
134
135Effect: Formloop renders 2 fields (first name + email) at the very start of the form. Each submission then includes `respondent: {name, email}` alongside `answers`.
136
137## Best practices
138
139**For multi-respondent (audit, panel):**
140- `persistent: true` is mandatory — otherwise only the first respondent counts.
141- `respondentField: email-name` unless you really want anonymous.
142- Keep under 30 blocks. Beyond, drop-off explodes. Split big audits across thematic forms.
143- Frame each item to audit with an `html` block that renders the item verbatim (title + ✅/❌ + real example) before the decision questions. Respondents shouldn't need to scroll a .md side-by-side.
144- For diffusion: email/Slack with a clear intro — why this form, estimated time, how results are used.
145
146**For ephemeral (1 decision):**
147- Cap at 6-8 blocks.
148- Prefer `html-pick` over `mc` when the choice is visual.
149- Use `html` blocks as intros, not `description`, when you want rich HTML.
150
151**For both:**
152- Explicit titles: "Hero design direction" beats "A few questions".
153- `required: false` on free-form `textarea` comments.
154- Test the form URL yourself before sending it.
155
156## Limitations
157
158- No conditional logic (skip-if/else). All questions are seen by all respondents.
159- No mid-form computation. Aggregate after submission.
160- HTML sanitization is strict. Use `scale-preview` for interactivity.
161- Spec TTL 24h, ephemeral response TTL 1h. Restart of the local server wipes everything (in-memory store).
162- No push notifications to respondents — sending email/Slack is on the caller.
163- 1 form = 1 spec. To iterate, create a new form (the old one stays accessible).
164
165## Self-hosting
166
167By default, `npx formloop` runs entirely on `127.0.0.1:3847` with an in-memory store. If you want persistence across restarts, set `KV_REST_API_URL` + `KV_REST_API_TOKEN` to point at an Upstash Redis (or any Upstash-compatible KV).
168
169To require API auth (multi-user shared server), set `WEBHOOK_SECRET=<long-random-string>` and pass it as the `x-webhook-secret` header on every `/api/forms*` call.