general8 min read

Building a Code Interpreter Workflow with the Responses API

Step-by-step tutorial for building a data analysis workflow with OpenAI's Responses API and Code Interpreter. Container creation, file upload, tool execution.

By DataStoryBot Team

Building a Code Interpreter Workflow with the Responses API

OpenAI's Responses API replaced the Assistants API as the primary way to build agentic workflows. If you need an LLM that can write Python, execute it in a sandbox, read files, and return computed results — this is the API you use. The code_interpreter tool runs code in an ephemeral container, and the Responses API orchestrates the conversation around it.

This article walks through building a complete data analysis workflow from scratch: creating containers, uploading files, executing analysis via the Responses API, and retrieving generated charts. This is the same pattern DataStoryBot uses internally to turn uploaded CSVs into narratives with visualizations.

The Architecture

Three OpenAI API surfaces work together:

Containers API manages sandboxed execution environments. You create a container, upload files to it, and download output files from it. Each container has a 20-minute TTL anchored to last activity.

Responses API is the inference layer. You send a prompt to GPT-4o with the code_interpreter tool enabled and a reference to your container. The model decides when to write and run code, observes the output, and continues until it has an answer.

Files on containers are how data flows in and out. You upload CSVs before analysis and download generated PNGs and CSVs after.

Create Container  →  Upload Files  →  Responses API (prompt + code_interpreter)
                                              ↓
                                    Model writes Python
                                              ↓
                                    Container executes it
                                              ↓
                                    Model reads stdout + generated files
                                              ↓
                                    Returns text response + file references
                                              ↓
                                    Download files before TTL expires

Step 1: Create a Container

The container is your sandbox. All file uploads and code execution happen inside it.

curl -X POST https://api.openai.com/v1/containers \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "analysis-session",
    "expires_after": {
      "anchor": "last_activity",
      "minutes": 20
    }
  }'

Response:

{
  "id": "ctr_abc123def456",
  "name": "analysis-session",
  "status": "active",
  "expires_after": {
    "anchor": "last_activity",
    "minutes": 20
  }
}

The 20-minute TTL is a hard limit. Every API call that references the container resets the clock. After 20 minutes of inactivity, the container and all its files are permanently deleted.

In Python using the OpenAI SDK:

from openai import OpenAI

client = OpenAI()

container = client.containers.create(
    name="analysis-session",
    expires_after={"anchor": "last_activity", "minutes": 20}
)
print(f"Container: {container.id}")

Step 2: Upload Files

Send your dataset to the container. The file becomes accessible to any code that runs inside it.

curl -X POST "https://api.openai.com/v1/containers/${CONTAINER_ID}/files" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -F "file=@sales_data.csv"

Response:

{
  "id": "file-xyz789",
  "filename": "sales_data.csv",
  "bytes": 524288,
  "created_at": 1711324800
}

Python SDK:

with open("sales_data.csv", "rb") as f:
    file = client.containers.files.create(
        container_id=container.id,
        file=f
    )
print(f"Uploaded: {file.filename} ({file.bytes} bytes)")

You can upload multiple files to the same container. They all share the same filesystem, so the model's code can read any of them.

Step 3: Execute Analysis via the Responses API

This is the core step. You send a prompt to the Responses API with the code_interpreter tool configured to use your container. The model writes Python, the container executes it, and the model reads the results.

curl -X POST https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-4o",
    "instructions": "You are a data analyst. Analyze the uploaded CSV. Find the 3 most interesting patterns. For each, create a chart using matplotlib with dark_background style and #141414 facecolor. Save charts as PNG files.",
    "input": [
      {
        "role": "user",
        "content": [
          {
            "type": "text",
            "text": "Analyze sales_data.csv. Focus on trends, anomalies, and segment differences."
          }
        ]
      }
    ],
    "tools": [
      {
        "type": "code_interpreter",
        "container": {
          "id": "'"${CONTAINER_ID}"'",
          "type": "auto"
        }
      }
    ]
  }'

The type: "auto" tells the API to let the model decide when to invoke Code Interpreter. In practice, with a data analysis prompt and a file in the container, it will always run code.

Python SDK equivalent:

response = client.responses.create(
    model="gpt-4o",
    instructions=(
        "You are a data analyst. Analyze the uploaded CSV. "
        "Find the 3 most interesting patterns. For each, create a "
        "publication-quality chart using matplotlib with dark_background "
        "style, #141414 facecolor, 150 DPI, figsize 10x6. "
        "Save charts as PNG files."
    ),
    input=[
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "Analyze sales_data.csv. Focus on trends, anomalies, and segment differences."
                }
            ]
        }
    ],
    tools=[
        {
            "type": "code_interpreter",
            "container": {"id": container.id, "type": "auto"}
        }
    ]
)

What happens inside

The model receives your prompt and the file listing from the container. It writes a Python script — typically starting with pd.read_csv(), then inspection, then analysis, then visualization. The container executes the code. The model reads stdout and any generated files, then formulates a text response summarizing the findings.

If the code throws an error, the model reads the traceback and retries with fixed code. This self-correction loop usually handles common issues like date parsing errors, encoding problems, or missing columns.

Step 4: Parse the Response

The Responses API returns a structured output array. You need to extract the text analysis and the file references.

# Extract the text analysis
text_parts = []
for item in response.output:
    if item.type == "message":
        for content in item.content:
            if hasattr(content, "text"):
                text_parts.append(content.text)

analysis_text = "\n".join(text_parts)
print(analysis_text)

# Extract file IDs from code interpreter outputs
file_ids = []
for item in response.output:
    if item.type == "code_interpreter_call":
        for result in item.results:
            if result.type == "files":
                for f in result.files:
                    file_ids.append(f.file_id)

print(f"Generated {len(file_ids)} files")

The response output contains interleaved message items (the model's text) and code_interpreter_call items (the code it wrote and its results). The file references inside code_interpreter_call results point to files that now exist in the container.

Step 5: Download Generated Files

Charts and any other files the code created live in the container. Download them before the TTL expires.

curl -o chart_1.png \
  "https://api.openai.com/v1/containers/${CONTAINER_ID}/files/${FILE_ID}/content" \
  -H "Authorization: Bearer $OPENAI_API_KEY"

Python SDK:

for i, file_id in enumerate(file_ids):
    content = client.containers.files.content(container.id, file_id)
    filename = f"chart_{i+1}.png"
    with open(filename, "wb") as f:
        f.write(content.read())
    print(f"Saved: {filename}")

How DataStoryBot Wraps This Pattern

DataStoryBot uses this exact infrastructure but abstracts away the container management, response parsing, and prompt engineering. Understanding the raw workflow makes it clear what the abstraction buys you.

Raw Responses APIDataStoryBot API
Create container + upload file + parse metadataPOST /api/upload (one call)
Craft prompt + call Responses API + parse free-form textPOST /api/analyze (returns structured JSON with 3 story angles)
Second Responses API call + parse narrative + extract file IDsPOST /api/refine (returns narrative + chart metadata)
Download from OpenAI container (needs API key)GET /api/files/{containerId}/{fileId} (no key needed)

The DataStoryBot equivalent of the five steps above:

import requests

BASE_URL = "https://datastory.bot"

# Steps 1-2 in one call
with open("sales_data.csv", "rb") as f:
    upload = requests.post(
        f"{BASE_URL}/api/upload",
        files={"file": ("sales_data.csv", f, "text/csv")}
    ).json()

container_id = upload["containerId"]

# Step 3: structured analysis
stories = requests.post(
    f"{BASE_URL}/api/analyze",
    json={"containerId": container_id}
).json()

# Steps 4-5: narrative + charts
result = requests.post(
    f"{BASE_URL}/api/refine",
    json={
        "containerId": container_id,
        "selectedStoryTitle": stories[0]["title"]
    }
).json()

print(result["narrative"])

for chart in result["charts"]:
    img = requests.get(
        f"{BASE_URL}/api/files/{container_id}/{chart['fileId']}"
    )
    with open(f"{chart['fileId']}.png", "wb") as f:
        f.write(img.content)

No OpenAI API key. No container lifecycle code. No response parsing. Three HTTP calls.

Multi-Turn Conversations and State Persistence

The Responses API supports multiple calls against the same container. The Python environment persists between calls — DataFrames loaded in turn 1 are still in memory for turn 2. This is how DataStoryBot implements the analyze-then-refine pattern: the refine step builds on the state and computed variables from the analyze step without re-parsing the CSV.

This has a practical implication for your own workflows: if you need iterative analysis (explore, then drill down, then generate visuals), use the same container across all calls. Each Responses API call adds to the execution state rather than starting fresh.

Practical Tips

Handle container expiry. The 20-minute TTL is the most common failure mode. If you get a 404 or "not found" error, the container is gone. Your code should detect this and prompt the user to re-upload.

Specify chart styling explicitly. Without explicit instructions, every run produces different-looking charts. Include dark_background style, #141414 facecolor, 150 DPI, figsize (10, 6) in the system prompt for consistency.

Ask for structured output. Instead of "analyze this data," say "return exactly 3 findings, each with a title, a 2-sentence summary, and one chart." The model follows structural constraints well.

Provide domain context. "This is e-commerce sales data. Seasonality and customer retention are high-value angles" produces better results than "analyze this CSV." The model uses domain hints to prioritize which patterns to investigate.

What to Read Next

For a comprehensive overview of Code Interpreter's capabilities, limitations, and available libraries, read the OpenAI Code Interpreter complete guide.

If you want to skip the OpenAI API integration and use the higher-level abstraction, the DataStoryBot getting started guide walks through the three-endpoint workflow that wraps everything described here.

To experiment without writing any code, upload a CSV to the DataStoryBot playground. Every chart and narrative you see there was generated by a Code Interpreter container running the exact pattern this article describes.

Ready to find your data story?

Upload a CSV and DataStoryBot will uncover the narrative in seconds.

Try DataStoryBot →