docs

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

  1. Ingest stores the Submission and sends submission.created, as it does today.
  2. If the Form is feedback and 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.
  3. Passed → webhooks with the submission.accepted event, a body with evaluation plus 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:

RuleWhat it does
min:NMinimum count of text characters in the payload. Defaults to 12.
-wordBlocks if the text (case-insensitive) contains the word.
anything elseGoes 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.

evaluate.ts
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.

accepted.json
{
  "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?

On this page