Designing Prompts for Repeatable Developer Workflows
From Chat Prompts to Workflow Prompts Many developers start using language models through chat: paste a diff, ask for a commit message, copy the answer. This works, but it is manual, inconsistent, and different every time. When the same task happens daily, such as writing commit messages, pull request descriptions, release notes, or review summaries, the prompt should become part of a repeatable workflow: a script or automated step that collects inputs, calls the model with a fixed prompt, checks the output, and presents it for approval.
Chat Prompt Workflow Prompt Written fresh each time Written once, versioned, and reused Input pasted manually Input collected automatically (for example from Git) Output read by the person who asked Output validated by code, then reviewed Results vary with phrasing Results follow a fixed contract No record Logged with prompt version This lesson explains how to design such workflows, using a commit message generator as a complete example.
Principles of Repeatable Workflow Prompts
Collect Inputs Automatically The workflow should gather its own inputs: the staged diff, the branch name, the list of changed files, the issue title. Manual copying introduces mistakes and makes results depend on what the person happened to paste.
Limit and Filter Inputs Diffs and logs can be very large, and they can contain secrets. Workflows should:
Exclude sensitive files, such as .env files and private keys. Limit size, and mark when content was truncated. Prefer summaries of large changes, such as file lists with line counts, over full content.
Use a Fixed, Versioned Prompt The prompt lives in a file or prompt library (Lesson 68), with a version number (Lesson 67). Changes are reviewed like code.
Define an Output Contract and Validate It The output has a defined format, such as a Conventional Commits message, and code checks it before anyone sees it.
Keep a Human in Control The developer reviews and can edit the result before it is used. Workflows propose; people decide, especially for anything that is published or changes shared state.
Fail Safely and Visibly If the model call fails or the output is invalid after retries, the workflow says so clearly and lets the developer continue manually. It must never silently produce a broken result.
Record What Happened Log the prompt version, model, input size, and validation result (Lesson 67), without logging sensitive content.
A Complete Example: Commit Message Generator The Output Contract The workflow produces messages in the Conventional Commits style, a widely used convention:
type(scope): short summary in lowercase
Optional body explaining what changed and why. Type Use feat A new feature fix A bug fix docs Documentation only refactor Code change that neither fixes a bug nor adds a feature test Adding or changing tests chore Maintenance, such as dependency updates perf Performance improvement The Prompt Write a commit message for the staged changes in .
Format:
- First line: type(scope): summary
- type is one of: feat, fix, docs, refactor, test, chore, perf
- scope is the main module or folder affected, in lowercase
- summary is in lowercase, imperative mood ("add", not "added"), at most 72 characters
- Second line: blank
- Body: 1 to 3 short lines explaining what changed and why, based only on the diff
Do not mention files or changes that are not in the diff. Return only the message.
{diff} Step 1: Collect and Filter the Input import subprocess
EXCLUDED_PATHS = [":(exclude).env", ":(exclude).pem", ":(exclude).key"]
def staged_diff(repo: str = ".", max_chars: int = 6000) -> str: """Return the staged diff, excluding sensitive files, limited in size.""" result = subprocess.run( ["git", "diff", "--staged", "--", ".", *EXCLUDED_PATHS], cwd=repo, capture_output=True, text=True, check=True, ) diff = result.stdout if not diff.strip(): raise SystemExit("No staged changes to describe.") if len(diff) > max_chars: diff = diff[:max_chars] + "\n[diff truncated]" return diff Code Explanation git diff --staged shows the changes that will be committed. Pathspecs such as :(exclude).env are Git's syntax for leaving files out of the diff. Here, environment files and key files are never sent to the model. check=True raises an error if the Git command fails, for example when the folder is not a Git repository. Size limit: large diffs are truncated with a visible marker (Lesson 71), which keeps cost predictable. Note: Path exclusion protects only the listed files. Secrets accidentally written inside ordinary source files would still be included. Use secret-scanning tools in your repository as well.
Step 2: Validate the Output import re
CONVENTIONAL = re.compile(r"^(feat|fix|docs|refactor|test|chore|perf)(([a-z0-9_-]+))?: [a-z].{0,70}$")
def validate_commit_message(message: str) -> list[str]: lines = message.strip().splitlines() problems = [] if not lines or not CONVENTIONAL.match(lines[0]): problems.append("First line must look like 'type(scope): summary' in lowercase, max ~80 characters") if len(lines) > 1 and lines[1].strip(): problems.append("Second line must be blank") return problems
for message in ["feat(pricing): add gst parameter to total", "Added GST", "fix: handle zero fee\nextra line without blank"]: print(repr(message.splitlines()[0]), validate_commit_message(message)) Output:
'feat(pricing): add gst parameter to total' [] 'Added GST' ["First line must look like 'type(scope): summary' in lowercase, max ~80 characters"] 'fix: handle zero fee' ['Second line must be blank'] Code Explanation The regular expression requires an allowed type, an optional lowercase scope in parentheses, a colon and space, and a summary starting with a lowercase letter. .{0,70} limits the rest of the summary, so the whole first line stays short. The blank second line separates the summary from the body, which is how Git tools display commit messages correctly. Step 3: Generate, Validate, Retry, and Ask for Approval Requirements: pip install openai and an OPENAI_API_KEY environment variable.
import json import time from datetime import datetime, timezone from pathlib import Path
from openai import OpenAI
client = OpenAI() MODEL = "gpt-5.4" # Replace with a model available in your account PROMPT_FILE = Path("prompts/commit_message.v1.txt") PROMPT_VERSION = "commit_message@1.0.0"
def generate_commit_message(diff: str, max_attempts: int = 2) -> str | None: instructions = PROMPT_FILE.read_text(encoding="utf-8") prompt = f"\n{diff}\n" for attempt in range(1, max_attempts + 1): start = time.perf_counter() response = client.responses.create(model=MODEL, instructions=instructions, input=prompt) message = response.output_text.strip() problems = validate_commit_message(message) log = {"time": datetime.now(timezone.utc).isoformat(), "prompt": PROMPT_VERSION, "attempt": attempt, "diff_chars": len(diff), "valid": not problems, "seconds": round(time.perf_counter() - start, 2)} with open("workflow_log.jsonl", "a", encoding="utf-8") as file: file.write(json.dumps(log) + "\n") if not problems: return message prompt = f"\n{diff}\n\n\nFix these problems:\n" + "\n".join(f"- {p}" for p in problems) return None
if name == "main": message = generate_commit_message(staged_diff()) if message is None: raise SystemExit("Could not generate a valid message. Please write it manually.") print("\nProposed commit message:\n") print(message) if input("\nUse this message? [y/N] ").strip().lower() == "y": subprocess.run(["git", "commit", "-m", message], check=True) else: print("Not committed. You can edit the message and commit manually.") Code Explanation Prompt from a versioned file The instructions are read from prompts/commit_message.v1.txt, so the prompt can be reviewed and changed without editing code. PROMPT_VERSION is recorded in every log entry.
Validation and retry Each output is validated. Problems are sent back once with specific feedback (Lesson 73). After max_attempts, the function returns None.
Logging Each attempt writes a JSON line with the time, prompt version, attempt number, diff size, validity, and duration. The diff and the message themselves are not logged.
Human approval The developer sees the proposed message and must explicitly answer y. Any other answer leaves the decision with the developer. The commit only happens after approval.
Safe failure If no valid message is produced, the script exits with a clear instruction to write the message manually.
Running It Save the script as commit_message.py, stage changes with git add, and run:
python commit_message.py Many teams add such scripts as Git aliases or editor commands, so the workflow is one command away.
Other Workflows Built the Same Way Workflow Automatic Input Output Contract Human Step Pull request description Commit messages and diff summary for the branch Sections: Summary, Changes, Testing, Risks Author edits before opening the PR Release notes Merged pull request titles since the last tag Grouped by New features, Improvements, Bug fixes (Lesson 80) Release manager approves Review assistant in CI Pull request diff Severity-ranked issues (Lesson 90) Posted as a suggestion comment; reviewers decide Changelog for API clients Diff of API schema files Breaking changes first (Lesson 96) API owner approves Test suggestions Changed functions Missing test cases (Lesson 94) Developer chooses which to write In every case, the pattern is the same: collect → filter → fixed prompt → validate → human decision → log.
Workflows in Continuous Integration When a workflow runs automatically in CI (for example, on every pull request):
Post suggestions, do not block merges based only on model output, unless the check has been validated thoroughly. Store the API key as a CI secret, never in the repository. Limit cost with size limits and by running only on relevant events. Treat repository content as untrusted input: a pull request could contain text designed to manipulate the model (Lessons 121–125). Delimit inputs and avoid giving the workflow permissions it does not need. Common Mistakes Mistake 1: Copy-Paste Workflows What it is: Developers paste diffs into a chat each time.
Why it happens: It is how the use of models usually starts.
What it causes: Inconsistent results and accidental sharing of sensitive files.
How to fix it: Script the workflow with automatic, filtered input collection.
Mistake 2: No Output Validation What it is: Using generated messages, notes, or comments directly.
Why it happens: Outputs usually look fine.
What it causes: Broken formats in commit history, changelogs, or automation.
How to fix it: Define an output contract and validate it in code.
Mistake 3: Fully Automatic Actions What it is: Workflows that commit, publish, or merge without approval.
Why it happens: Automation seems more efficient.
What it causes: Wrong or misleading content published under a developer's name.
How to fix it: Require explicit human approval for actions that change shared state.
Mistake 4: Sending Secrets in Inputs What it is: Diffs or logs that include environment files, keys, or tokens.
Why it happens: Inputs are collected without filtering.
What it causes: Secrets leave your environment.
How to fix it: Exclude sensitive paths, use secret scanning, and limit inputs to what is needed.
Mistake 5: Unversioned Prompts in Scripts What it is: Prompt text hard-coded and edited directly in scripts.
Why it happens: It is the quickest way to start.
What it causes: Unexplained changes in output and no way to compare versions.
How to fix it: Store prompts in versioned files and log the version with each run.
Best Practices Automate input collection. Gather inputs from Git, issue trackers, and files directly.
Filter and limit inputs. Exclude sensitive files, cap size, and mark truncation.
Use versioned prompt files. Review prompt changes like code.
Define and validate output contracts. Retry with feedback, then fail visibly.
Keep humans in control of shared actions. Propose, then approve.
Log runs without sensitive content. Prompt version, sizes, validity, timing.
Treat repository content as untrusted in CI. Delimit inputs and limit permissions.
Try It Yourself Exercise 1: Build the Commit Workflow Create prompts/commit_message.v1.txt with the prompt from this lesson, save the script, and use it for five real commits. Record how many messages were accepted unchanged, edited, or rejected.
Exercise 2: Pull Request Description Workflow Write a script that collects the commit messages on the current branch with git log main..HEAD --oneline and the list of changed files with git diff main --stat. Generate a pull request description with the sections Summary, Changes, Testing, and Risks, and validate that all four headings are present.
Exercise 3: Improve Filtering Extend EXCLUDED_PATHS for your project, for example to exclude lock files, generated files, and migration folders if they are noisy. Compare the diff size before and after filtering on a recent change, and check whether the generated commit messages improve.