Capstone: Build a Production Astra Workflow
- Complete one end-to-end Astra project from scratch
- Apply at least three concepts from this track
- Measure actual cost and quality of a production workflow
Choose Your Path
This capstone runs in two parallel tracks. Path A is for ChatGPT users who want a practical computer-use automation they can run every week without writing code. Path B is for developers who want a working Responses API agent that combines functions, file search, and structured outputs in a single call.
Choose the path that fits your context — but read both. The design principles in Path A (scoping, failure handling, success criteria) transfer directly to how you write system prompts for Path B agents. The cost measurement section at the end applies to anyone deciding whether to keep an Astra workflow in production.
Each path ends with a concrete artifact: Path A produces a tested, saved ChatGPT Project prompt. Path B produces a running Python script you can adapt for your own data.
Path A: Computer Use Workflow (ChatGPT)
The goal is a prompt that drives a weekly repetitive task from start to finish without you intervening. These five steps take a vague idea and turn it into a reliable, failure-aware automation.
Step 1: Pick a Real Task
Choose something you actually do every week — exporting a report, filling a spreadsheet from a website, moving files from one folder to another, copying data between two apps. The criteria:
- It has a clear start state (you know what the screen or file looks like before the task begins).
- It has a clear end state (you know exactly what "done" looks like).
- It takes 5–30 minutes manually. Too short and the setup overhead outweighs the value; too long and a single failure costs too much.
Good examples: compile last week's analytics into a summary sheet, pull pricing from a competitor's website into a comparison doc, move completed items from one project board column to another.
Step 2: Write the Prompt
A computer-use prompt has three required sections. Missing any one of them is the main reason first attempts fail:
- Start state. What is currently on screen or in the file system. Be specific: "Google Chrome is open to the Analytics dashboard, logged in, showing last 7 days."
- Success criteria. What "done" looks like in terms Astra can verify: "The file 'weekly-summary.xlsx' exists in ~/Downloads with columns Date, Sessions, Conversions, and Revenue filled in."
- Scope limits. What the model must NOT do: "Do not close any browser tab that was open before you started. Do not submit any form. Stop and ask if you reach a login screen."
Put these sections in this order. A prompt that opens with scope limits before describing the task reads backwards to the model and to you.
Step 3: Run It and Log Failures
Run the prompt on one real instance of the task. While it runs, note:
- Where did it pause or ask a clarifying question?
- Did it complete the task? If not, at which step did it stop?
- Did it do something unexpected that was within scope? (Worth noting — you may want to generalize that behavior.)
Keep a plain-text log. Three to five bullet points is enough. You're looking for the two most common failure modes across runs, not a complete execution trace.
Step 4: Revise for the Top Two Failure Modes
Take the two most significant failure points and add explicit handling for each. Typical patterns:
- UI element not found — add a fallback: "If you cannot find the Export button, look for a three-dot menu or right-click the table."
- Login or auth wall — add a stop rule: "If any page requires authentication that is not already in place, stop and report what you encountered."
- Ambiguous file path — make the start state more specific: "The file is in /Users/username/Documents/Reports, not the Downloads folder."
Revise the prompt and run it again. Two revision cycles is usually enough for a well-scoped task to reach reliable execution.
Step 5: Save to a ChatGPT Project
Once the prompt is working, open ChatGPT Projects and create a project for this workflow. Paste the final prompt into the project's system instructions. This makes two things possible:
- The context persists between runs — you don't re-paste the prompt each week.
- You can refine the instructions over time without losing prior run history.
What "done" looks like: you open the project, type one sentence ("run the weekly analytics export"), and Astra completes the task to your success criteria without you writing the full prompt again.
Path B: Responses API Agent (Developer)
This script combines file search, a custom function, structured outputs, and reasoning control in a single Responses API call. Run it against real data before calling it production-ready — the goal is a working agent, not a template.
The Complete Script
import openai
import json
client = openai.OpenAI() # reads OPENAI_API_KEY from environment
# ── 1. Create a vector store and upload a document ────────────────────────────
store = client.vector_stores.create(name="product-docs")
with open("product-catalog.pdf", "rb") as f:
client.vector_stores.file_batches.upload_and_poll(
vector_store_id=store.id,
files=[("product-catalog.pdf", f, "application/pdf")]
)
print(f"Vector store ready: {store.id}")
# ── 2. Define tools: file search + one custom function ────────────────────────
tools = [
{
"type": "file_search",
"vector_store_ids": [store.id]
},
{
"type": "function",
"name": "get_inventory_level",
"description": "Return current inventory count for a product SKU.",
"parameters": {
"type": "object",
"properties": {
"sku": {
"type": "string",
"description": "Product SKU, e.g. PRD-0042"
}
},
"required": ["sku"],
"additionalProperties": False
}
}
]
# ── 3. Define a structured output schema ──────────────────────────────────────
response_format = {
"type": "json_schema",
"json_schema": {
"name": "product_summary",
"strict": True,
"schema": {
"type": "object",
"properties": {
"sku": {"type": "string"},
"display_name": {"type": "string"},
"description": {"type": "string"},
"inventory": {"type": "integer"},
"in_stock": {"type": "boolean"}
},
"required": ["sku", "display_name", "description", "inventory", "in_stock"],
"additionalProperties": False
}
}
}
# ── 4. Call the Responses API ────────────────────────────────────────────────
def run_agent(user_query: str) -> tuple:
response = client.responses.create(
model="gpt-6-astra",
reasoning={"effort": "high"},
tools=tools,
response_format=response_format,
input=[
{
"role": "system",
"content": (
"You are a product catalog assistant. "
"Search the uploaded documents for product details, "
"then call get_inventory_level to check current stock. "
"Return your answer using the product_summary schema."
)
},
{"role": "user", "content": user_query}
]
)
# Handle function call turn if the model invoked get_inventory_level
for item in response.output:
if item.type == "function_call" and item.name == "get_inventory_level":
sku = json.loads(item.arguments)["sku"]
inventory_result = {"count": 42} # replace with real inventory lookup
# Continue the same reasoning turn with the function result
response = client.responses.create(
model="gpt-6-astra",
reasoning={"effort": "high"},
tools=tools,
response_format=response_format,
previous_response_id=response.id,
input=[{
"type": "function_call_output",
"call_id": item.call_id,
"output": json.dumps(inventory_result)
}]
)
# ── 5. Parse and validate the structured output ───────────────────────────
for item in response.output:
if item.type == "message":
result = json.loads(item.content[0].text)
return result, response.usage
raise RuntimeError("No message in response output")
# ── Run and log cost ──────────────────────────────────────────────────────────
query = "Give me a summary of the Aero-X standing desk, SKU PRD-0042"
result, usage = run_agent(query)
print(json.dumps(result, indent=2))
print(f"Tokens — input: {usage.input_tokens} output: {usage.output_tokens}")
Key notes on the script structure:
- Create the vector store once during setup and store the ID in config — not inline in the request loop. Re-uploading documents on every run discards the cache value you built.
"additionalProperties": Falseon the function parameters is required for strict function calling. Without it, the model may pass extra arguments that break your handler.previous_response_idcontinues the same reasoning turn for the function-result call. The model's prior context is preserved without re-sending the full input.- Strict structured outputs mean a schema violation surfaces as an API error before you try to parse it — caught at the call site, not buried in your validation logic.
Testing Your Workflow
Path A: Testing Computer Use
Run your prompt on five different real instances of the task. Vary the starting conditions on each run:
- A slightly different UI state than your prompt describes (a modal is open, a different tab is active).
- Edge-case data: an empty report, maximum number of rows, special characters in filenames.
- A starting state where a previous partial run left output behind.
Log each run: starting conditions, what Astra did, and whether the success criterion was met. Three out of five completions is a reasonable bar for a first version. Five out of five means the prompt is ready for unsupervised production use.
Path B: Testing the API Agent
Run your script on ten different queries that cover the expected input range. For each run, capture the output and the usage object:
test_queries = [
"Summarize the Aero-X standing desk, SKU PRD-0042",
"What is the stock level for the ErgoChair Pro, SKU PRD-0107",
# add 8 more covering your expected input range
]
results = []
for query in test_queries:
try:
result, usage = run_agent(query)
assert isinstance(result["sku"], str)
assert isinstance(result["inventory"], int)
assert isinstance(result["in_stock"], bool)
results.append({
"query": query,
"status": "ok",
"input_tokens": usage.input_tokens,
"output_tokens": usage.output_tokens,
"result": result
})
except Exception as e:
results.append({"query": query, "status": "fail", "error": str(e)})
with open("test-results.json", "w") as f:
json.dump(results, f, indent=2)
ok = sum(1 for r in results if r["status"] == "ok")
print(f"Acceptance rate: {ok}/{len(results)}")
Review test-results.json after all ten runs. A failure rate above 20% means the schema, function definition, or system prompt needs revision before the workflow runs unsupervised.
Measuring Cost and Quality
Every Responses API response includes a usage object. Reading it on every call is the only way to know what a workflow costs in production rather than in theory.
The three fields you need:
usage.input_tokens— all input tokens, including file search context injected from the vector store.usage.output_tokens— all output tokens, including the structured JSON response.usage.input_token_details.cached_tokens— input tokens served from the prompt cache at $1/M instead of $10/M.
def compute_cost(usage) -> float:
PRICE_INPUT = 10 / 1_000_000 # $10 per million tokens
PRICE_CACHED = 1 / 1_000_000 # $1 per million (cached reads)
PRICE_OUTPUT = 50 / 1_000_000 # $50 per million tokens
cached = getattr(usage.input_token_details, "cached_tokens", 0)
fresh = usage.input_tokens - cached
return (fresh * PRICE_INPUT +
cached * PRICE_CACHED +
usage.output_tokens * PRICE_OUTPUT)
Log compute_cost(usage) on every production call alongside a pass/fail flag for the output. After a week of real traffic, compute average cost per accepted task and accepted-task rate. Use them together to decide when to revise:
medium; re-measureAn accepted-task rate above 95% with stable cost is the threshold for an unsupervised production workflow. Below 80%, revise before running unattended. Between 80% and 95%, the right threshold depends on the stakes — a customer-facing output requires a higher bar than an internal report.
What's Next
You've built one of the two core Astra workflows. The most productive next step depends on which path you took.
If you took Path A — you have a working computer-use automation saved in a ChatGPT Project. When you're ready to run computer use programmatically, the OpenAI computer use API guide picks up from here. Lessons 9–14 in this track give you the Responses API foundation that guide assumes.
If you took Path B — your agent is running. The natural expansion is adding the computer-use tool to your tools list, which lets the agent interact with real interfaces rather than structured data. That capability is also covered in the OpenAI computer use documentation.
Two primary sources from OpenAI that this track cited throughout:
- GPT-6 Astra System Card — the full safety evaluation, including the recurrent depth monitoring limitations summarized in Lesson 1.
- OpenAI Preparedness Framework — the risk evaluation process that determines what capabilities each model ships with.
This track covered GPT-6 Astra end to end: architecture and access tiers (Lesson 1), ChatGPT features for non-developers (Lessons 2–8), the Responses API for developers (Lessons 9–14), and this capstone. Use the cost measurement tooling from this lesson and Lesson 14 to stay grounded in what the model actually delivers on your specific workload.
- Path A: a well-scoped computer use prompt + failure handling is all you need for a real automation
- Path B: reasoning_effort, functions, file search, and structured outputs compose cleanly into a production agent
- Measuring cost per task in production tells you exactly when to dial reasoning up or down