skills/netlify-forms-preflight/SKILL.md
Verify a Netlify-hosted form is registered and accepting submissions BEFORE distributing any offline asset (flyer, QR code, business card, print ad, physical signage) that points at the form. Catches the silent failure mode where Netlify only indexes forms seen during a build with form-detection enabled — submissions made before the form is registered hit the static 404 and are irrecoverable. Use this skill whenever the user is about to print/distribute QR codes, flyers, business cards, or any offline material that drives leads to a Netlify-hosted form; whenever a form "works in prod but no submissions are showing up"; whenever the user says "I enabled forms but can't see anything"; or proactively after merging any PR that ships a new Netlify-forms-backed lead capture page. Also covers verifying email notifications are wired so leads are not silently sitting in a dashboard nobody checks.
npx skillsauth add razbakov/skills netlify-forms-preflightInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Netlify Forms has a subtle failure mode that burns lead-gen launches: forms only catch submissions after Netlify's build-time HTML scanner has seen them in a deploy with form detection enabled. Before that moment, the form's POST target does not exist server-side — visitors' browsers send the POST, Netlify returns the static 404 page, and the submission vanishes. There is no buffer, no queue, no recovery path.
This matters most when offline assets are already in circulation. Once QR codes are printed and flyers are handed out, every scan is a one-shot chance to capture a lead. If the form isn't registered the moment the first flyer hits the street, those leads are gone.
The fix is a five-minute pre-flight check. Do it BEFORE printing. If it's too late — flyers are out — do it now and at least bound the loss to what's already been missed.
Invoke this skill when:
data-netlify="true", form-name, or Netlify form detection in a troubleshooting contextnetlify status. If not logged in, user runs netlify login.my-site) or the public URL is enough — the skill looks up the site_id./contact), and the value of the form's name attribute / form-name hidden input.Netlify's API indexes everything by site_id (a UUID), not by site name. List the user's sites and filter:
netlify api listSites 2>&1 | python3 -c "
import sys, json
d = json.load(sys.stdin)
q = '<name-or-url-substring>' # e.g. 'razbakov' or 'mysite.com'
for s in d:
if q.lower() in (s.get('name','') + s.get('url','')).lower():
print(s.get('name'), '|', s.get('id'), '|', s.get('url'))
"
Capture the UUID into a shell var for reuse: SITE_ID=<uuid>.
Fetch the page and confirm the form markup is present in the prerendered HTML. Netlify's scanner only sees what's in the static HTML at build time — forms that only exist after client-side Vue/React hydration will not be detected.
curl -s "https://<site-domain>/<form-page>" -o /tmp/page.html
# Confirm form markup is server-rendered (not Vue/React-only):
grep -oE '<form[^>]*name="[^"]+"[^>]*>' /tmp/page.html
grep -oE 'data-netlify="[^"]*"|netlify-honeypot|name="form-name"' /tmp/page.html | sort -u
grep -oE '<input[^>]*name="[^"]+"' /tmp/page.html | sort -u
What to check:
<form> element with a name= attribute matching what the skill expectsdata-netlify="true" OR the netlify boolean attribute (both work)<input name="form-name" value="<form-name>"> — Netlify requires this on submission so it knows which form the POST belongs toIf any of the above is missing, the form will never be detected. Fix the markup and redeploy before continuing.
This is the step that catches the most common failure.
netlify api listSiteForms --data="{\"site_id\":\"$SITE_ID\"}" 2>&1 | python3 -c "
import sys, json
forms = json.load(sys.stdin)
if not forms:
print('NO FORMS REGISTERED — submissions will 404')
for f in forms:
print(f\"{f['name']:<30} submissions={f['submission_count']:<4} last={f.get('last_submission_at')} created={f['created_at']}\")
"
Decision tree:
created_at is recent → Registration is fresh. Any submissions that happened before created_at were lost. Note this timestamp — the skill will need to report it to the user so they can estimate the loss.created_at is old → Form is live and has been for a while. Continue to Step 5.If the form is not registered:
https://app.netlify.com/sites/<site-name>/settings/forms → toggle "Form detection" on.cd <project-dir>
git commit --allow-empty -m "chore: trigger rebuild for Netlify form detection"
git push
Alternative without a git push: the user can click "Trigger deploy → Deploy site" in the Netlify dashboard.
The form is registered — but there's one more thing that can go wrong: the POST URL. Netlify forms POST to / by default (or to the form's action URL). If the site is behind redirects, edge functions, or a custom router that swallows root POSTs, submissions still die.
Test with a real POST using an obviously-test payload:
TS=$(date +%s)
curl -sS -o /tmp/resp.html -w "HTTP %{http_code}\n" \
-X POST "https://<site-domain>/" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "form-name=<form-name>&<field1>=preflight-probe-$TS&<field2>=...&bot-field="
What to expect:
HTTP 200 → submission accepted. Go to Step 6 and verify it arrived.HTTP 303 or 302 → also normal; Netlify often redirects to a success page.HTTP 404 → form is NOT handled at this URL. Either the action attribute points somewhere else, or a redirect rule is eating the POST. Fix and retry.HTTP 400 → Netlify's spam filter blocked it. Look less suspicious (don't put "debug" / "test" in the payload), or proceed — real submissions with normal content will pass.Include a bot-field= empty value in the POST if the form has data-netlify-honeypot="bot-field" — the honeypot expects that field to exist but be empty.
netlify api listSiteSubmissions --data="{\"site_id\":\"$SITE_ID\"}" 2>&1 | python3 -c "
import sys, json
subs = json.load(sys.stdin)
print(f'total: {len(subs)}')
for s in subs[:5]:
print(s.get('created_at'), '|', s.get('form_name'), '|', json.dumps(s.get('data', {}), ensure_ascii=False)[:200])
"
The probe submission should appear at the top with a timestamp matching the POST from Step 5. If it does, the form is fully wired: server accepts POSTs, registers them against the right form, stores the data.
If the submission is NOT there despite a 200 from Step 5, something is eating it between Netlify's edge and the form processor. This is rare — most likely a site redirect rule or an overly aggressive spam filter. Check the Netlify deploy logs for a "form submission" line.
Form submissions default to dashboard-only visibility. If the user doesn't check the Netlify dashboard every day, leads rot there. Before declaring the form ready:
https://app.netlify.com/sites/<site-name>/forms.This step is not API-automatable via the public netlify CLI — it's a dashboard-only action. Hand it to the user with the exact URL.
Produce a concise status report:
Netlify Forms pre-flight — <site-name>
Form: <form-name> (id <form-id>)
Registered: <created_at> → submissions before this timestamp were lost
Submission count: <n>
Probe POST (Step 5): HTTP <code>, arrived in dashboard: yes/no
Email notifications: configured / NOT YET — user action required
Dashboard: https://app.netlify.com/sites/<site-name>/forms
Ready to distribute: YES / NO
Blockers: <list>
If distribution has already happened and registration is recent, explicitly state the loss window:
Flyers distributed before <created_at> scanned into a 404. Submissions from that window are not recoverable.
curl -sI <url> | grep -iE 'server|cache-status' — Netlify Edge hits show up in cache-status.For any lead-gen form that drives from offline assets (where failure is especially costly), instrument a second source of truth alongside Netlify Forms:
posthog.capture('form_submit', {...}) (or GA equivalent) on client-side form submit, before the POST. If Netlify silently breaks, analytics still shows the intent.This is not part of the pre-flight check itself — it's a hardening recommendation to make the next failure recoverable (see the form-signup-recovery skill for how to back-fill from a Resend email log).
tools
--- name: handoff description: Get an agent past a browser/UI wall it can't (or must not) cross on its own — a login-gated dashboard, a CAPTCHA, a 2FA prompt, an API that keeps rejecting the write, or an irreversible click that policy says a human must make. This skill is an ESCALATION LADDER, not a first move: it tells you to try the automated browser surfaces FIRST (Chrome-in-Claude, computer-use, an autonomous browser sub-agent) and only fall back to the Handoff app — a wrapper browser that h
documentation
Summarize one or more YouTube videos from their links. Use this whenever the user pastes a youtube.com or youtu.be URL (or several) and wants to know what it's about — phrasings like "summarize to telegram", "tldr these videos", "what do these say", "summary of this talk", or just dropping links with no instruction at all. Fetches each video's real transcript via yt-dlp (not the page text, which never contains the transcript), cleans the captions, and writes a per-video summary. Default delivery is Telegram; honor any other surface the user names ("to my notes", "just here in chat", "email it"). Trigger even when the user only pastes bare links — bare YouTube links almost always mean "tell me what's in these".
data-ai
Daily Digest — Chief-of-Staff role consolidates the six top-managers into one Telegram message to the Commander, instead of six. Implements the protocol from agent-proactivity.md.
development
Seed a new or empty Instagram account with a 9-post grid (3×3) so the profile looks established the moment a new visitor lands. Designed for festivals, new businesses, product launches, conferences, communities — any time an empty IG profile would hurt conversion from external traffic (QR scans, flyer drops, cross-promo). Generates assets via /image-from-gemini (per content-publishing rules — never HTML), writes captions with hashtag sets, and outputs a posting order + cadence plan. Trigger generously: phrases like '9 posts for instagram', 'fill my IG', 'starter grid', 'launch grid', 'instagram seed', '9-post grid', 'IG account not to look empty', 'first instagram posts', 'feed bootstrap', '3x3 grid', 'instagram launch content'. Even if the user mentions only one piece (just the images, just the captions, just the order), use this skill — the grid only works as an integrated bundle.