Template

Schema‑First Spec Kit (JSON Schema + Notion Checklist + Prompt Pack)

Copy‑ready kit to generate, validate, and operationalize your service specs: a strict JSON Schema starter, a Notion checklist template that mirrors the spec, and a prompt pack with a self‑critique pass so bad specs never reach your build.

This kit turns messy client paragraphs into a build‑ready spec you can enforce before you write a single Zap/Scenario node. Use it in three passes: (1) generate a draft spec with Structured Outputs, (2) validate it against JSON Schema (external microservice or in‑platform), and (3) spin up a Notion checklist that mirrors the spec’s acceptance criteria and constraints.

What you need: a provider that supports structured outputs (OpenAI/Gemini/Claude — provider details vary), your automation platform (Make or n8n), and a Notion workspace. Swap placeholders in [BRACKETS] and ship.

JSON Schema Starter (copy‑paste)

Copy this schema as your baseline and customize the enums/descriptions to your typical client requests. It’s strict by default (additionalProperties: false) so your validator will catch unplanned fields immediately.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://schemas.headcountzero.me/service-spec/v1",
  "title": "Service Build Spec",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "spec_version", "project", "service", "inputs", "outputs",
    "acceptance_criteria", "test_plan", "operational"
  ],
  "properties": {
    "spec_id": {
      "type": "string",
      "pattern": "^spec_[a-z0-9]{8}$",
      "description": "Generated identifier (e.g., spec_3fa4c9b2)."
    },
    "spec_version": {
      "type": "string",
      "pattern": "^v\\d+\\.\\d+\\.\\d+$",
      "description": "Semantic version of this spec (e.g., v1.0.0)."
    },
    "created_at": { "type": "string", "format": "date-time" },
    "project": {
      "type": "object",
      "additionalProperties": false,
      "required": ["client_name", "project_name", "owner_email", "due_date"],
      "properties": {
        "client_name": { "type": "string", "minLength": 2, "description": "[CLIENT_NAME]" },
        "project_name": { "type": "string", "minLength": 2, "description": "[PROJECT_NAME]" },
        "owner_email": { "type": "string", "format": "email", "description": "[OWNER_EMAIL]" },
        "due_date": { "type": "string", "format": "date", "description": "[YYYY-MM-DD]" },
        "budget_usd": { "type": "number", "minimum": 0, "description": "[BUDGET_USD]" }
      }
    },
    "service": {
      "type": "object",
      "additionalProperties": false,
      "required": ["type", "deliverable"],
      "properties": {
        "type": {
          "type": "string",
          "enum": [
            "automation", "data_pipeline", "web_scraper", "chatbot", "integration"
          ],
          "description": "[SERVICE_TYPE]"
        },
        "deliverable": { "type": "string", "minLength": 3, "description": "[DELIVERABLE_SUMMARY]" },
        "latency_target_s": { "type": "number", "minimum": 0, "description": "[MAX_LATENCY_SECONDS]" },
        "run_frequency": { "type": "string", "enum": ["on_demand", "hourly", "daily", "weekly"], "description": "[RUN_FREQUENCY]" }
      }
    },
    "llm": {
      "type": "object",
      "additionalProperties": false,
      "required": ["provider", "model"],
      "properties": {
        "provider": { "type": "string", "enum": ["openai", "anthropic", "google"], "description": "[LLM_PROVIDER]" },
        "model": { "type": "string", "minLength": 2, "description": "[MODEL_NAME]" },
        "context_budget_tokens": { "type": "integer", "minimum": 1000, "description": "[CONTEXT_TOKENS]" },
        "temperature": { "type": "number", "minimum": 0, "maximum": 1, "default": 0 }
      }
    },
    "inputs": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["name", "type"],
        "properties": {
          "name": { "type": "string", "description": "[INPUT_NAME]" },
          "type": {
            "type": "string",
            "enum": ["api", "csv", "webhook", "notion", "airtable", "google_sheets", "email"]
          },
          "auth": { "type": "string", "enum": ["none", "api_key", "oauth2", "basic"], "description": "[AUTH_METHOD]" },
          "example_record": { "type": "object", "description": "Representative input for testing." }
        }
      }
    },
    "outputs": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["destination", "format"],
        "properties": {
          "destination": { "type": "string", "enum": ["notion", "airtable", "gdrive", "webhook", "email"] },
          "format": { "type": "string", "enum": ["json", "markdown", "html", "csv"] },
          "fields": { "type": "array", "items": { "type": "string" }, "minItems": 1 }
        }
      }
    },
    "acceptance_criteria": {
      "type": "array",
      "minItems": 1,
      "items": { "type": "string", "minLength": 3 }
    },
    "constraints": {
      "type": "array",
      "items": { "type": "string", "minLength": 3 },
      "description": "Business or technical constraints (e.g., 'No PII leaves EU')."
    },
    "edge_cases": {
      "type": "array",
      "items": { "type": "string", "minLength": 3 }
    },
    "test_plan": {
      "type": "object",
      "additionalProperties": false,
      "required": ["test_cases"],
      "properties": {
        "test_cases": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["id", "description", "input_fixture", "expected_output"],
            "properties": {
              "id": { "type": "string", "pattern": "^TC-\\d{2,}$" },
              "description": { "type": "string" },
              "input_fixture": { "type": "object" },
              "expected_output": { "type": "object" }
            }
          }
        }
      }
    },
    "operational": {
      "type": "object",
      "additionalProperties": false,
      "required": ["error_budget_pct", "retry_policy", "logging_required", "pii_handling"],
      "properties": {
        "error_budget_pct": { "type": "number", "minimum": 0, "maximum": 100, "description": "Allowable failure rate (e.g., 1.0)." },
        "retry_policy": {
          "type": "object",
          "additionalProperties": false,
          "required": ["max_retries", "backoff_s"],
          "properties": {
            "max_retries": { "type": "integer", "minimum": 0 },
            "backoff_s": { "type": "number", "minimum": 0 }
          }
        },
        "logging_required": { "type": "boolean" },
        "pii_handling": { "type": "string", "enum": ["none", "masked", "encrypted"] }
      }
    }
  }
}

Tip: keep this schema short enough to fit within your chosen model’s context window when you include long client notes. If it gets large, host it and fetch at runtime.

Example Valid Spec (for testing)

Use this to dry‑run your validator and checklist generation.

{
  "spec_id": "spec_7a3c9e01",
  "spec_version": "v1.0.0",
  "created_at": "2026-04-18T15:00:00Z",
  "project": {
    "client_name": "[CLIENT_NAME]",
    "project_name": "[PROJECT_NAME]",
    "owner_email": "owner@[CLIENT_DOMAIN].com",
    "due_date": "[YYYY-MM-DD]",
    "budget_usd": 2500
  },
  "service": {
    "type": "automation",
    "deliverable": "Route inbound Webflow form leads, enrich with Clearbit, and post to Notion + Slack",
    "latency_target_s": 8,
    "run_frequency": "on_demand"
  },
  "llm": {
    "provider": "openai",
    "model": "gpt-4.1",
    "context_budget_tokens": 200000,
    "temperature": 0
  },
  "inputs": [
    { "name": "Webflow Form", "type": "webhook", "auth": "none", "example_record": { "email": "lead@example.com", "company": "Acme" } }
  ],
  "outputs": [
    { "destination": "notion", "format": "json", "fields": ["email", "company", "enrichment.score"] }
  ],
  "acceptance_criteria": [
    "Leads appear in Notion within 8 seconds of form submit",
    "Missing domain triggers enrichment retry up to 2x before marking 'Unenriched'",
    "Slack alert posts only on first failure per lead"
  ],
  "constraints": [
    "No PII sent to third‑party enrichment without consent",
    "Run only 9am–7pm local time"
  ],
  "edge_cases": [
    "Disposable emails",
    "International phone formats"
  ],
  "test_plan": {
    "test_cases": [
      {
        "id": "TC-01",
        "description": "Happy path with full data",
        "input_fixture": { "email": "good@acme.com", "company": "Acme" },
        "expected_output": { "status": "Enriched" }
      }
    ]
  },
  "operational": {
    "error_budget_pct": 1.0,
    "retry_policy": { "max_retries": 2, "backoff_s": 3 },
    "logging_required": true,
    "pii_handling": "masked"
  }
}

Prompt Pack: Schema‑First Generation + Self‑Critique

Use these prompts to produce a spec that matches your schema, then self‑critique common failure modes before you validate.

System prompt (all providers):

You are a senior automation architect. Produce a JSON object that conforms to the provided JSON Schema. Do not include explanations. Fill unknowns with explicit TODO items; do not invent data. Prefer conservative defaults for latency and error budgets.

User prompt (insert your details):

Context: [SERVICE_CONTEXT]
Client notes:
"""
[CLIENT_PARAGRAPHS]
"""
Constraints to respect: [CONSTRAINTS_SUMMARY]
Targets: latency ≤ [MAX_LATENCY_S]s, budget ≤ $[BUDGET_USD]

Return only JSON that matches this schema:
[SCHEMA_JSON]

Self‑critique pass (run immediately after generation):

Review the draft spec for these failure modes and fix them before returning final JSON only:
- Missing or vague acceptance_criteria (must be testable statements)
- Underspecified inputs/outputs (add example_record and fields)
- Latency/ budget conflicts (adjust or flag TODO)
- Constraints that contradict deliverable (resolve or flag)
Output final JSON only.

Provider notes:

  • OpenAI/Gemini support structured outputs with a subset of JSON Schema. Use their response formatting features to strictly enforce the structure. Exact keyword support varies — check model docs before relying on specific keywords.
  • Claude supports long context and JSON‑style outputs; follow current provider guidance for schema‑constrained responses. Provider capabilities change — verify against docs on your date.

Notion Checklist Template (database + task block)

Set up one database to hold tasks per spec. Create these properties:

  • Name (Title)
  • Status (Select): Not started, In progress, Blocked, Done
  • Severity (Select): Blocker, Major, Minor
  • Source (Select): Spec, Validator
  • Due (Date)
  • Assignee (People)
  • Spec ID (Text)
  • Project (Text)

Template: “Build — [PROJECT_NAME]” (paste the block below into the page as a template and duplicate per spec):

Checklist — Pre‑build gate (Source: Validator)
[ ] Spec [SPEC_ID] passes JSON Schema validation (0 errors)
[ ] Test cases defined (≥ 1) and fixtures present

Checklist — From acceptance_criteria (Source: Spec)
[ ] Verify: [CRITERION_1]
[ ] Verify: [CRITERION_2]
[ ] Verify: [CRITERION_3]

Checklist — From constraints (Source: Spec)
[ ] Enforce: [CONSTRAINT_1]
[ ] Enforce: [CONSTRAINT_2]

Checklist — Operational (Source: Spec)
[ ] Implement retry policy: [MAX_RETRIES] retries, [BACKOFF_S]s backoff
[ ] Mask/handle PII as: [PII_HANDLING]
[ ] Set run frequency: [RUN_FREQUENCY]

Automation mapping (Make or n8n): when valid=false from your validator, create one Notion task per error with these fields:

  • Name: "Fix schema error at [ERROR_PATH] — [ERROR_MESSAGE]"
  • Status: Blocked
  • Severity: Blocker (for required failures), Major (for type/enum), Minor (for minLength/maxLength)
  • Source: Validator
  • Spec ID: [SPEC_ID]
  • Due: [TODAY+1]

When valid=true, upsert the template tasks above by expanding arrays:

  • acceptance_criteria → tasks named "Verify: [TEXT]"
  • constraints → tasks named "Enforce: [TEXT]"
  • operational.retry_policy → task named "Implement retry policy: [MAX_RETRIES]/[BACKOFF_S]"
  • operational.pii_handling → task named "Mask/handle PII as: [PII_HANDLING]"

Error → Notion Task Mapping (ready to paste)

Standardize how you transform validator output into actionable tasks.

Expected validator response:

{
  "valid": false,
  "errors": [
    { "path": "/project/owner_email", "keyword": "format", "message": "must match format 'email'" },
    { "path": "/acceptance_criteria/0", "keyword": "minLength", "message": "too short" },
    { "path": "/service/type", "keyword": "enum", "message": "must be one of ..." }
  ]
}

Mapping rules:

  • keyword = "required" → Severity: Blocker; Name: "Add required field at [PATH]"
  • keyword in {"type", "enum", "format"} → Severity: Major; Name: "Fix [KEYWORD] at [PATH]: [MESSAGE]"
  • keyword in {"minLength", "maxLength", "minimum", "maximum"} → Severity: Minor; Name: "Adjust constraint at [PATH]: [MESSAGE]"

Always set Source = Validator and include Spec ID.

Optional description field on the task:

Spec: [SPEC_ID]
Path: [PATH]
Reason: [MESSAGE]
Suggested fix: [AUTO_SUGGESTION_OR_TODO]

Optional: Cloudflare Worker Validator (copy‑deploy)

If you prefer an external gate callable from Make/n8n, deploy this Worker (Ajv is not compatible with Workers’ runtime; use a Workers‑compatible validator):

worker.js

import { Validator } from '@cfworker/json-schema';

export default {
  async fetch(request) {
    if (request.method !== 'POST') return new Response('Method Not Allowed', { status: 405 });
    const { data, schema, specId, secret } = await request.json();
    if (secret !== Deno.env.get('SHARED_SECRET')) return new Response('Unauthorized', { status: 401 });

    const v = new Validator(schema);
    const result = v.validate(data);

    return Response.json({
      valid: result.valid,
      errors: result.errors?.map(e => ({ path: e.instanceLocation, keyword: e.keyword, message: e.error })) ?? [],
      specId
    });
  }
}

.env (Workers secret)

SHARED_SECRET=[YOUR_SHARED_SECRET]

POST body from Make/n8n:

{
  "data": [SPEC_JSON],
  "schema": [SCHEMA_JSON],
  "specId": "[SPEC_ID]",
  "secret": "[YOUR_SHARED_SECRET]"
}

Response fields match the Notion mapping section above.