Gate
Once a Submission lands, Noonmark can evaluate it. If it passes, it sends submission.accepted so your AI can start the work. Noonmark never writes code in your repo.
The flow
- Ingest stores the Submission and sends
submission.created, as it does today. - If the Form is
feedbackand the gate is on, it evaluates in this order: the evaluator URL, otherwise the Workspace's OpenAI key, otherwise local rules. Category is Auto classify, a separate feature. - Passed → webhooks with the
submission.acceptedevent, a body withevaluationplus the raw Submission. Failed → it stays in the Studio and never reaches your destination.
A survey Form never enters this flow. Turning the gate on there sends no
acceptance.
OpenAI
Paste an API key sk-… under Workspace → Settings. The Gate calls
gpt-4o-mini (or OPENAI_MODEL in the environment) with the Form's instructions, in
order, and asks for JSON: pass, reason, summary. If Auto classify is also on, it
asks for category in the same reply.
The category appears in the Submission list when Auto classify is on. The default list is bug, feature, question, praise, spam, other — or whatever you write on the Form.
While the Studio has no login, anyone who opens the Studio can reach the key. Don't use a shared production key.
Instructions
One per line in the Form's Integration. The model follows the sequence. With no key
and no URL, the min:N and -word lines still work as a local gate.
Local gate
On the Form's Integrations tab. Criteria, one rule per line:
| Rule | What it does |
|---|---|
min:N | Minimum count of text characters in the payload. Defaults to 12. |
-word | Blocks if the text (case-insensitive) contains the word. |
| anything else | Goes in the remote evaluator's body; the local gate ignores it. |
Remote evaluator
Optional. Noonmark POSTs and your service decides. 5 s timeout. 2xx with a pass
boolean. Any other result becomes error in the Studio and sends no acceptance.
export async function POST(request: Request) {
const { submission, criteria } = await request.json();
// your model decides
return Response.json({
pass: true,
reason: "actionable",
summary: submission.payload.message,
});
}Your destination
Create a webhook with the submission.accepted event. It is the same signed POST as
the webhooks. Your agent (Cursor, CI, a bot) listens on that URL and
starts the build.
{
"event": "submission.accepted",
"evaluation": {
"pass": true,
"reason": "actionable_bug",
"summary": "The save button disappears on mobile.",
"category": "bug"
},
"submission": {
"id": "7e583034-1a0a-47d5-ab72-74d4541f8486",
"formId": "form_demo",
"receivedAt": 1788182240254,
"origin": "https://app.acme.com",
"userAgent": "Mozilla/5.0 …",
"payload": { "email": "ana@acme.com", "message": "…" }
}
}The same Submission never produces two acceptances: the gate status is stored, and each webhook + Submission pair gets exactly one delivery.
What did you think of this content?
Email both ways. Invitations send a survey's public page and store nothing. Inbound email gives a feedback Form an address; what arrives is stored as a Submission, and status changes answer in the thread.
Auto classify
Noonmark's first AI feature. After ingest, the model picks a category and writes it on the Submission. It decides no acceptance and sends no webhook.