Most prompting advice is still wrong for serious work.

It tells you to “be clear,” “give context,” and “iterate with the model.” That's fine for brainstorming. It's weak for shipping code into a real repo. If you prompt like you're chatting, you get chatty failure modes. Missing files. Silent assumptions. Weird formatting. Half-right code that looks plausible until tests fail.

That's why the current spec-driven debate matters. Critics call it “waterfall with markdown,” “spec drift,” and “token-burning ceremony.” Sometimes they're right. A bloated spec can waste your time just as fast as a vague prompt. But the backlash is partly aimed at the wrong target. The core issue isn't structure. It's bad structure. Sloppy prompting feels fast until you need the result to be repeatable.

The useful shift is simpler than the discourse makes it sound. Stop treating prompts as conversations. Treat them as functions. Give them a signature, a body, a return type, and error handling. Then version them, test them, and change them when they stop behaving.

That framing keeps showing up in developer circles, including the Reddit thread on “specs beat prompts”. It's the same idea you see in Simon Willison's agentic engineering work and in Drew Breunig's writing on the rise of spec-driven development. If you want a practical example of this mindset in a product workflow, How MeshBase uses Claude Opus for Next.js is worth reading because it grounds AI use in real implementation constraints instead of vague prompt theater.

Table of Contents

Stop Chatting With Your AI and Start Programming It

The shortest way to improve AI output is to stop asking for “help” and start defining an interface.

A conversational prompt invites the model to fill in gaps. Sometimes that's useful. In coding work, it's usually where things go sideways. The model guesses your file boundaries, your architecture, your naming rules, and your tolerance for risk. Then you waste time correcting decisions you never asked it to make.

What changes when you think in functions

A function call is boring on purpose. It expects specific inputs. It does one job. It returns something you can reason about. When you treat prompts the same way, you stop writing motivational essays to a model and start writing contracts.

That contract matters because structured prompting improves reliability in practice. Atlan reports that structured prompt engineering processes can reduce AI errors by up to 76% compared to unstructured inputs in its prompt engineering guide.

Practical rule: If a task would break production when done wrong, don't phrase it like a casual request.

That doesn't mean every task needs a giant spec. It means your prompt should carry the same seriousness as any other interface you depend on.

A prompt function has four parts

Think about a common brownfield task. You want an agent to add a rate limit banner to an existing billing settings page without touching unrelated UI.

A weak prompt looks like this:

  • “Add a rate limit warning to the billing page.”
  • “Use the existing design system.”
  • “Make sure tests still pass.”

That's not a contract. That's a wish.

A function-style prompt looks more like this:

Part What it does Example
Signature Defines required inputs target files, feature goal, constraints
Body Describes behavior exact scope, design rules, test expectations
Return type Defines output shape unified diff, JSON, file list, markdown report
Error handling Controls failure mode ask for missing context, refuse unsafe changes

You're no longer saying “please do a thing.” You're saying “given these inputs, produce this class of result, or fail in a known way.”

That's the difference between prompting for novelty and prompting for dependable work.

The Prompt-as-Function Paradigm Explained

A prompt function is just an API contract written in natural language.

It still uses English. It still benefits from examples. But the mental model changes everything. You stop judging prompts by whether they sound smart. You judge them by whether they produce stable outputs under the same inputs.

A diagram explaining the Prompt-as-Function paradigm in AI, showing inputs, processing, contracts, and structured code outputs.

What changes when you think in functions

The biggest difference is that ambiguity becomes a bug, not a style choice.

Enterprise AI teams already treat prompts this way. One industry analysis describes layered prompt architecture where persistent system prompts are separated from task-specific user prompts so teams can define baseline behavior, constraints, context, and policy boundaries. The same source notes that prompts can explicitly specify structure, length, JSON or table formats, and tone. That's why production prompting looks more like interface design than chatting, as explained in Airia's write-up on structured prompt architecture.

In practice, this means your coding prompt can have a stable outer shell and a task-specific inner payload.

For example:

  • System layer sets repo safety rules, coding standards, and refusal behavior.
  • Task layer supplies the feature request, file paths, acceptance criteria, and output schema.
  • Evaluation layer checks whether the result matches your rubric.

That's not overkill. That's how you reduce “it worked yesterday” prompt drift.

A prompt function has four parts

Here's the pattern in plain terms.

  1. Signature You define the parameters the agent must receive. Repo path. Files in scope. User-visible outcome. Constraints. Missing any of these, the call is invalid.

  2. Body You define what counts as correct behavior. This includes business rules, architecture boundaries, naming rules, and what not to touch.

  3. Return type You specify the output format. Not “give me the code.” Instead: return a markdown plan, a JSON object, or a list of file patches with rationale.

  4. Error handling You tell the model what to do when the contract can't be satisfied. Ask a clarifying question. Return needs_input. Refuse unsafe assumptions.

A prompt becomes reusable when another person can run it without guessing what you meant.

Here's a stripped-down signature:

type GenerateFeaturePrompt = {
  featureName: string
  goal: string
  filesInScope: string[]
  filesOutOfScope: string[]
  acceptanceCriteria: string[]
  outputFormat: "json"
  onMissingContext: "ask"
}

That's the model. Not magic. Just stricter software thinking applied to language interfaces.

Designing Input Contracts for Your Prompts

Reliability problems usually start before the model writes a single token.

Bad input contracts force the agent to fill gaps with guesses about your codebase, your standards, and your intent. In practice, that means wasted runs, noisy diffs, and code that technically works while violating the shape of the system.

A cartoon engineer feeding data blocks into a machine representing a validated AI input contract process.

Treat the prompt input the way you treat a function signature in production code. If a parameter matters, declare it. If a dependency changes behavior, name it. If the task has preconditions, make them explicit. Prompting gets more reliable when you stop treating context as a blob and start treating it as an interface.

For coding agents, a usable input contract usually includes five fields:

  • Task definition
    Describe the user-visible change in concrete terms. “Add a password reset request form at /forgot-password that sends an email if the account exists” gives the agent a target it can implement and test.

  • Scope boundaries
    State which files may change and which parts of the system are off limits. If edits are restricted to app/routes/forgot-password.tsx and lib/email.ts, say so.

  • Relevant context
    Include facts the model cannot infer safely from partial repo context. Validation library, auth provider, component conventions, test runner, security constraints.

  • Acceptance criteria
    Define done in terms an agent can execute or check. Tight criteria prevent “looks right” outputs that fail review. A good pattern is documented in acceptance criteria agents can actually execute.

  • Known unknowns
    List assumptions that still need confirmation. If an environment variable, API contract, or business rule is unclear, expose that uncertainty instead of letting the agent invent around it.

Prompt design starts to strongly resemble spec-driven development, and that overlap makes some developers uncomfortable. Good. It should. A spec is a control surface. It limits improvisation. That feels slower if you are used to chat-style prompting, but it is faster than reviewing three plausible implementations built on three different hidden assumptions.

A common failure case is: “build feature X using the existing patterns.” In a real repo, “existing patterns” often means a mix of old decisions, half-finished migrations, and local exceptions. The agent will choose one. You might not agree with that choice, and now you are debugging prompt ambiguity instead of shipping the feature.

Use a contract like this:

Task: Add forgot-password request flow

Goal:
Let users submit email addresses to request a reset link.

Files in scope:
- app/routes/forgot-password.tsx
- app/routes/api.forgot-password.ts
- lib/email/reset.ts

Files out of scope:
- signup flow
- login flow
- database schema
- shared button components

Constraints:
- use Zod for validation
- use existing toast pattern from app/routes/login.tsx
- do not reveal whether an email exists
- keep copy plain and short

Acceptance criteria:
- form validates email on submit
- API returns same success message for existing and non-existing accounts
- reset email sender is called only for valid addresses
- tests cover success and invalid-input paths

If blocked:
- ask one clarifying question
- do not invent missing environment variables

This format works because it gives the model fewer chances to improvise in the wrong place. It also gives you something you can version, review, and update as the codebase changes.

That versioning point matters. Once prompts become part of delivery, they need the same hygiene as code. Store them with the feature spec. Tie contract changes to code reviews. If a prompt starts failing after a framework upgrade or architecture shift, diff the contract the same way you would diff an API schema. Teams arguing about SDD often miss this practical middle ground. You do not need a giant requirements process. You need inputs stable enough that the agent is operating against a known contract instead of repo folklore.

The same rule applies outside code generation. Tasks with hidden assumptions break when the prompt leaves those assumptions unstated. Coding agents just make the failure visible faster, because the output hits your repository instead of a chat window.

A quick visual example helps if you're building this habit into your day-to-day process.

Enforcing Output Contracts and Validation

A prompt without an output contract is not a function. It is a suggestion.

That distinction matters once an agent starts writing files, opening pull requests, or feeding another step in your pipeline. If the return shape is loose, the model fills the gap with prose, caveats, and invented confidence. That is fine in chat. It is expensive in a repo.

A table comparing the advantages and disadvantages of using structured output contracts for AI prompt engineering.

Reliable teams define return types the same way they define API responses. For agent work, the common patterns are:

  • Structured JSON for validators, CI checks, and chained agent steps
  • Markdown with fixed headings for human review and approval
  • File-by-file patch plans when implementation needs a checkpoint
  • Status objects that expose blocked or unsafe work clearly

A basic output contract might look like this:

{
  "status": "success | needs_input | unsafe",
  "summary": "short description",
  "files_to_change": [
    {
      "path": "app/routes/forgot-password.tsx",
      "change": "add form and validation"
    }
  ],
  "open_questions": [],
  "acceptance_checks": []
}

That gives you something you can parse, diff, validate, and reject before the model touches code outside scope.

Define failure states up front

A lot of prompt failures are contract failures. The model was never told how to fail.

If you only describe the happy path, the agent will try to satisfy it even when the repo is missing context, the request conflicts with constraints, or a required file does not exist. That behavior is one reason prompt engineering keeps getting treated like magic instead of software design. In practice, this is reliability engineering. You are reducing ambiguous states before they hit production.

Use explicit failure rules:

  • Ask instead of guessing when required files, schema details, or environment assumptions are missing
  • Return needs_input when architecture context is incomplete
  • Return unsafe when the request violates scope, security, or system constraints
  • List broken assumptions instead of smoothing over them

Do not reward confidence. Reward contract compliance.

This is also where prompt-as-function work starts to overlap with spec-driven development. An agent should not just produce output. It should produce output that can be checked against acceptance criteria agents can actually execute. That is the useful part of SDD for small teams. Not extra ceremony. Fewer hidden assumptions, clearer failure modes, and faster review.

Treat output schemas like versioned interfaces

Once a prompt feeds real delivery work, the schema becomes part of your system boundary. Changing planned_changes to files or renaming needs_input to blocked is a breaking change if downstream tooling expects the old contract.

Store prompt schemas next to the code they affect. Review them in pull requests. Add a small validator before any agent output reaches the next step. If you are already investing in writing documentation for AI agents, this is the missing piece that keeps those docs operational instead of aspirational.

A practical validator can be simple:

  • parse the response
  • reject unknown keys
  • verify enum values
  • fail if required arrays are missing
  • stop the run if status !== success

That pattern sounds strict because it is. Strictness is the point. Agents are easier to work with when the contract leaves less room for improvisation.

A practical output contract for code work

Here's a prompt ending I use often:

Return JSON only.

Schema:
{
  "status": "success | needs_input | unsafe",
  "summary": "string",
  "planned_changes": [
    {
      "filePath": "string",
      "reason": "string"
    }
  ],
  "questions": ["string"],
  "risks": ["string"]
}

Rules:
- Do not include markdown fences
- Do not include any keys not listed above
- If a required assumption is missing, set status to "needs_input"
- If the request conflicts with scope boundaries, set status to "unsafe"

This works because the agent is no longer deciding what a good answer looks like. The contract already decided. That makes retries cleaner, automated checks easier, and regressions easier to spot when a prompt changes over time.

Practical Examples with Modern AI Coding Agents

The structure stays mostly the same across tools. The differences show up in how much repo context they already hold, how strictly they follow formatting, and how much hand-holding they need when a task gets messy.

A comparison table outlining key features and use cases for AI agents: Cursor, Claude Code, Codex, and Gemini.

One task four agents

Take a concrete task. You want a new UserProfileCard component in a React app, plus tests.

The job:

  • build UserProfileCard.tsx
  • add UserProfileCard.test.tsx
  • support name, email, and optional avatarUrl
  • follow existing Tailwind conventions
  • don't edit unrelated layout files

The function-style wrapper for all agents looks like this:

Function:
generate_component_with_test(inputContract) -> structured_result

Input contract:
- componentName: UserProfileCard
- targetDir: src/components/profile
- props:
  - name: string
  - email: string
  - avatarUrl?: string
- filesInScope:
  - src/components/profile/*
- filesOutOfScope:
  - src/pages/*
  - src/layouts/*
- constraints:
  - use TypeScript
  - use existing Tailwind utility style
  - no new dependencies
- acceptanceCriteria:
  - renders name and email
  - renders fallback avatar state
  - test covers optional avatarUrl
- outputContract:
  - return markdown with headings:
    - Summary
    - Files Created
    - Code
    - Test Notes
- onFailure:
  - ask for one missing pattern example before proceeding

A base prompt signature you can adapt

The adaptation is usually small.

Agent What to emphasize
Cursor mention nearby files and repo patterns explicitly
Claude Code give broader architectural context and stronger scope limits
Codex keep instructions compact and concrete
Gemini be explicit about format and file boundaries

A few practical notes:

  • Cursor often benefits from “copy this pattern from file X” style instructions.
  • Claude Code is strong when you give it a bigger spec plus constraints.
  • Codex tends to do better when you trim fluff and keep the contract tight.
  • Gemini usually responds better when output shape is spelled out clearly.

Those aren't universal truths. They're working heuristics.

If you're planning changes before handing them to an agent, codebase-aware AI planning is the right upstream habit. Better planning shrinks prompt entropy.

Judge results with a rubric not vibes

Once prompts start acting like functions, you can evaluate them like software.

Braintrust recommends judging outputs against versioned rubrics on dimensions like correctness, relevance, and safety, and it suggests calibrating automated judge systems with roughly 100 to 200 human-scored examples before relying on automated scoring in its article on prompt evaluation.

That matters because “Cursor nailed it” isn't a test result. It's a mood.

A lightweight rubric for the component task could score:

  • Correctness
    Does the component satisfy the prop contract and compile?

  • Scope obedience
    Did the agent stay inside allowed files?

  • Style fit
    Does the code match local conventions?

  • Test completeness Are the acceptance criteria covered?

Good prompts don't just produce answers. They produce answers you can grade.

If you care about the docs side of this, the GitDocAI article on writing documentation for AI agents pairs well with this workflow because prompt reliability and agent-readable docs are tightly connected.

Integrating Prompts into a Spec-Driven Workflow

Single prompts help. Prompt libraries help more.

A significant benefit manifests when your prompt functions transition from random chat history to a workflow with stable inputs, reusable formats, and reviewable outputs.

Screenshot from https://tekk.coach

Prompt libraries beat prompt improvisation

A spec-driven workflow gives you the missing middle layer between “I have an idea” and “the agent changed twelve files.”

That middle layer usually contains:

  • the problem statement
  • scope boundaries
  • assumptions
  • acceptance criteria
  • validation scenarios
  • output expectations

That's why spec-first and spec-anchored approaches keep coming up in this space. Birgitta Böckeler's taxonomy is useful here because it separates rigid spec-first thinking from lighter spec-anchored workflows. Addy Osmani's six-section spec format is useful for the same reason. It gives solo builders a repeatable shape without pretending every task needs full ceremony.

A lot of teams also want agent handoff points that are built into the workflow itself. If you're comparing patterns, Donely's page on built-in AI agent integrations is a helpful contrast because it shows one end of the integration spectrum, where tooling tries to sit closer to the execution layer.

Where specs fit in the loop

The prompt-as-function model fits cleanly into a spec-driven loop:

  1. You define the problem.
  2. You capture constraints and acceptance criteria.
  3. You turn that into a structured spec.
  4. You hand the spec to an agent as the function body.
  5. You validate the output against the spec.

That's where a short internal artifact helps more than a long prompt thread. If you need a lean version, the minimal spec format is enough for most solo builder workflows.

Simon Willison's broader agentic engineering work lands on the same principle. Prompts behave more like software components than one-off messages once you reuse them across tasks and models. Drew Breunig's writing on SDD adds trend context, but the useful part isn't the trend. It's the operational discipline.

What matters is this. You write the contract once. Then you refine it based on failures instead of starting from scratch every time.

Anti-Patterns and When to Break the Rules

The failure mode in spec-driven work is not too little rigor. It is applying production-grade ceremony to throwaway tasks.

Solo founders hit this fast. A one-line copy tweak does not need a versioned prompt, a JSON schema, and a regression suite. A migration that touches auth, billing, and webhooks probably does. Reliability work starts with choosing where failures are expensive, then spending structure there.

When strict structure helps

Treat prompts like versioned functions when the output will be reused, reviewed, or executed against a live system.

Use tight input and output contracts for:

  • Core API edits where one wrong field breaks clients
  • Authentication changes where silent assumptions create risk
  • Brownfield refactors where the repo already has scar tissue
  • Multi-file features where acceptance criteria need coordination

OpenAI recommends pinning model snapshots and running evaluations as model behavior changes in its prompt engineering documentation. That is the software angle many teams miss. The prompt is only one part. The stable result comes from the prompt, the model version, the checks, and the failure loop.

This is also where the broader SDD debate gets messy. Critics are right about one thing. If every task starts with a long markdown spec, you are recreating process overhead. Supporters are right about something too. If prompts drive code changes, they need the same controls you already use for code: versioning, review, tests, and rollback.

When to loosen up

Use loose prompting when the cost of being wrong is low and the goal is exploration.

That includes:

  • naming ideas
  • UI brainstorming
  • rough boilerplate
  • exploring implementation options before you commit

There is a trade-off. Research on prompt design for software tasks found that more structure does not always produce better results, especially in brownfield codebases where generic prompts, or no detailed prompt at all, can outperform task-specific instructions in some settings, as discussed in the review of prompt engineering trade-offs.

The practical rule is simple.

Use the minimum structure needed to stop expensive guessing.

I have seen agents do better with a short spec plus direct access to the code than with a long prompt full of examples copied from a different repo. Extra detail can narrow the search space in the wrong direction. It can also freeze a bad assumption early.

The maintenance cost is real

Strict prompts create maintenance work. That is the part the prompt-as-function crowd sometimes understates.

Model updates can make a few-shot prompt weaker. Old examples can anchor the agent to patterns your codebase no longer wants. A contract that looked precise six weeks ago can drift out of sync with the spec, the tests, or the product itself. The same review literature notes that automated prompt methods vary across models and tasks, and that zero-shot can beat few-shot in some cases.

So treat prompt assets like code with a support burden.

  • use specs for alignment
  • use prompts for execution
  • use evaluations for trust

If a prompt contract keeps breaking, refactor it like code. Delete stale instructions. Cut decorative examples. Tighten the interface. Split one overloaded prompt into two narrower ones if needed. Sometimes the right fix is even smaller than that. Stop passing a giant spec and pass a typed input object plus one explicit acceptance check.

Breaking the rules is fine when you do it on purpose. Freeform chat is useful during discovery. It is a bad default for repeatable delivery. The line is not philosophical. It is operational. If you need the same result twice, put structure around it. If you only need a fast sketch, keep it loose.


Connect your GitHub repo. Describe the problem. Get a structured spec. Ship. Tekk.coach

Part of the Spec-Driven Development pillar — a 52-page honest playbook on shipping with AI coding agents.