---
title: Workflow agent code step examples
slug: agent-platform/docs/workflow-agent-code-step-examples
createdAt: 2026-09-24T15:00:00.000Z
---

In Mark (Optimizely Agent Platform), these examples provide handler code to copy into a code step and adapt. Every example lists the input variables to declare and the sample data for a test run. Each one also gives the handler and the value it returns.

For the configuration fields and the handler contract these examples rely on, go to [Workflow agent code steps](docId:_s_XSUJ9dfsunf2I-xB_8).

## Filter and aggregate data

This example filters a list of orders against a dollar threshold and returns the matching orders with a total.

Declare the following input variables:

| **Name**    | **Type** | **Mode**  | **Description**                                   |
| ----------- | -------- | --------- | ------------------------------------------------- |
| `orders`    | Array    | Automatic | The list of order objects from the upstream step. |
| `threshold` | Number   | Fixed     | Minimum order amount to include.                  |

Enter the following sample data:

```json
{
  "orders": [
    { "id": "ORD-001", "customer": "Acme Corp", "amount": 250 },
    { "id": "ORD-002", "customer": "Globex Inc", "amount": 75 },
    { "id": "ORD-003", "customer": "Initech", "amount": 500 },
    { "id": "ORD-004", "customer": "Umbrella LLC", "amount": 30 }
  ],
  "threshold": 100
}
```

Use the following Python handler:

```python
def handler(input, ctx):
    orders = input["orders"]
    threshold = input["threshold"]

    filtered = [o for o in orders if o["amount"] > threshold]
    total = sum(o["amount"] for o in filtered)

    return {
        "filtered_orders": filtered,
        "count": len(filtered),
        "total": total,
        "summary": f"Filtered {len(filtered)} of {len(orders)} orders above ${threshold}"
    }
```

The handler returns the following value:

```json
{
  "filtered_orders": [
    { "id": "ORD-001", "customer": "Acme Corp", "amount": 250 },
    { "id": "ORD-003", "customer": "Initech", "amount": 500 }
  ],
  "count": 2,
  "total": 750
}
```

The handler keeps only the orders with an `amount` greater than `threshold`, computes the total, and returns a structured dictionary. Mark lifts the `summary` value into the execution log and removes it from the return value.

## Format output for a downstream agent

This example reshapes raw data into a structured brief that a downstream specialized agent consumes. An upstream agent gathers customer feedback entries, and the code step groups them for a summarization agent.

Declare the following input variables:

| **Name**           | **Type** | **Mode**  | **Description**                                                |
| ------------------ | -------- | --------- | -------------------------------------------------------------- |
| `feedback_entries` | Array    | Automatic | The list of customer feedback objects from the upstream agent. |
| `product_name`     | String   | Fixed     | The product name to include in the brief.                      |

Enter the following sample data:

```json
{
  "feedback_entries": [
    { "source": "Support ticket", "text": "Dashboard loads slowly when filtering by date range.", "sentiment": "negative" },
    { "source": "NPS survey", "text": "Love the reporting features. Very intuitive.", "sentiment": "positive" },
    { "source": "Support ticket", "text": "Export to CSV is missing column headers.", "sentiment": "negative" },
    { "source": "NPS survey", "text": "Would be great to have dark mode.", "sentiment": "neutral" }
  ],
  "product_name": "Acme Tech"
}
```

Use the following Python handler:

```python
def handler(input, ctx):
    entries = input["feedback_entries"]
    product = input["product_name"]

    by_sentiment = {}
    for entry in entries:
        sentiment = entry.get("sentiment", "unknown")
        by_sentiment.setdefault(sentiment, []).append(entry)

    sections = []
    for sentiment in ["negative", "neutral", "positive"]:
        items = by_sentiment.get(sentiment, [])
        if not items:
            continue
        lines = [f"- [{item['source']}] {item['text']}" for item in items]
        sections.append(f"### {sentiment.capitalize()} ({len(items)})\n" + "\n".join(lines))

    brief = f"# Customer Feedback Summary for {product}\n\n"
    brief += f"Total entries: {len(entries)}\n\n"
    brief += "\n\n".join(sections)

    return {
        "formatted_brief": brief,
        "counts": {s: len(items) for s, items in by_sentiment.items()},
        "summary": f"Formatted {len(entries)} feedback entries for {product}"
    }
```

The handler returns the following value:

```json
{
  "formatted_brief": "# Customer Feedback Summary for Acme Tech\n\nTotal entries: 4\n\n### Negative (2)\n- [Support ticket] Dashboard loads slowly when filtering by date range.\n- [Support ticket] Export to CSV is missing column headers.\n\n### Neutral (1)\n- [NPS survey] Would be great to have dark mode.\n\n### Positive (1)\n- [NPS survey] Love the reporting features. Very intuitive.",
  "counts": { "negative": 2, "positive": 1, "neutral": 1 }
}
```

A downstream specialized agent receives `formatted_brief` as its input. The agent produces a polished summary without parsing raw data itself.

## Validate and clean data

This example validates a list of email addresses and separates the valid entries from the invalid ones.

Declare the following input variable:

| **Name** | **Type** | **Mode**  | **Description**                            |
| -------- | -------- | --------- | ------------------------------------------ |
| `emails` | Array    | Automatic | List of email address strings to validate. |

Enter the following sample data:

```json
{
 "emails": [
   "alice@example.com",
   "bob@",
   "charlie@company.org",
   "not-an-email",
   "dana@sub.domain.co.uk",
   "@missing-local.com",
   "eve@example"
 ]
}
```

Use the following Python handler:

```python
import re

def handler(input, ctx):
    emails = input["emails"]
    pattern = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")

    valid = []
    invalid = []

    for email in emails:
        email_clean = email.strip().lower()
        if pattern.match(email_clean):
            valid.append(email_clean)
        else:
            invalid.append(email_clean)

    return {
        "valid_emails": valid,
        "invalid_emails": invalid,
        "valid_count": len(valid),
        "invalid_count": len(invalid),
        "summary": f"Validated {len(emails)} emails: {len(valid)} valid, {len(invalid)} invalid"
   }
```

The handler returns the following value:

```json
{
  "valid_emails": ["alice@example.com", "charlie@company.org", "dana@sub.domain.co.uk"],
  "invalid_emails": ["bob@", "not-an-email", "@missing-local.com", "eve@example"],
  "valid_count": 3,
  "invalid_count": 4
}
```

Downstream steps read `valid_emails` and proceed with verified addresses only.

## Parse CSV text into structured data

This example parses a comma-separated values (CSV) string from an upstream step into an array of objects. It also converts numeric columns.

Declare the following input variable:

| **Name**   | **Type** | **Mode**  | **Description**                          |
| ---------- | -------- | --------- | ---------------------------------------- |
| `csv_text` | String   | Automatic | The raw CSV text including a header row. |

Enter the following sample data:

```json
{
  "csv_text": "name,department,salary\nAlice,Engineering,95000\nBob,Marketing,72000\nCharlie,Engineering,105000\nDana,Sales,68000"
}
```

Use the following Python handler:

```python
import csv
import io

def handler(input, ctx):
    csv_text = input["csv_text"]
    reader = csv.DictReader(io.StringIO(csv_text))

    records = []
    for row in reader:
        cleaned = {}
        for key, value in row.items():
            try:
                cleaned[key] = int(value)
            except (ValueError, TypeError):
                try:
                    cleaned[key] = float(value)
                except (ValueError, TypeError):
                    cleaned[key] = value
        records.append(cleaned)

    columns = reader.fieldnames or []

    return {
        "records": records,
        "columns": columns,
        "row_count": len(records),
        "summary": f"Parsed {len(records)} rows with {len(columns)} columns"
    }
```

The handler returns the following value:

```json
{
  "records": [
    { "name": "Alice", "department": "Engineering", "salary": 95000 },
    { "name": "Bob", "department": "Marketing", "salary": 72000 },
    { "name": "Charlie", "department": "Engineering", "salary": 105000 },
    { "name": "Dana", "department": "Sales", "salary": 68000 }
  ],
  "columns": ["name", "department", "salary"],
  "row_count": 4
}
```

The handler uses the `csv` module from the Python standard library. It detects the header row and converts numeric values from strings. Downstream steps filter, sort, or aggregate the returned array of dictionaries directly.

## Score and rank items with JavaScript

This example scores a list of sales leads against weighted criteria and returns them ranked by score.

Declare the following input variables:

| **Name**  | **Type** | **Mode**  | **Description**                                                                |
| --------- | -------- | --------- | ------------------------------------------------------------------------------ |
| `leads`   | Array    | Automatic | Lead objects with `company_size`, `engagement_score`, and `has_budget` fields. |
| `weights` | Object   | Fixed     | Scoring weight for each criterion.                                             |

Enter the following sample data:

```json
{
  "leads": [
    { "name": "Acme Corp", "company_size": 500, "engagement_score": 85, "has_budget": true },
    { "name": "Startup Inc", "company_size": 15, "engagement_score": 92, "has_budget": false },
    { "name": "BigCo Ltd", "company_size": 5000, "engagement_score": 40, "has_budget": true },
    { "name": "MidRange LLC", "company_size": 200, "engagement_score": 70, "has_budget": true }
  ],
  "weights": {
    "company_size": 0.3,
    "engagement": 0.5,
    "budget": 0.2
  }
}
```

Use the following JavaScript handler:

```javascript
async function handler(input, ctx) {
    const leads = input.leads;
    const weights = input.weights;

    function normalizeSize(size) {
        return Math.min(size / 1000, 1) * 100;
    }

    const scored = leads.map(lead => {
        const sizeScore = normalizeSize(lead.company_size) * weights.company_size;
        const engagementScore = lead.engagement_score * weights.engagement;
        const budgetScore = (lead.has_budget ? 100 : 0) * weights.budget;

        return {
            ...lead,
            score: Math.round(sizeScore + engagementScore + budgetScore),
            breakdown: {
                size: Math.round(sizeScore),
                engagement: Math.round(engagementScore),
                budget: Math.round(budgetScore)
            }
        };
    });

    scored.sort((a, b) => b.score - a.score);

    return {
        ranked_leads: scored,
        top_lead: scored[0].name,
        summary: `Scored ${scored.length} leads. Top lead: ${scored[0].name}`
    };
}
```

The handler returns the following value:

```json
{
  "ranked_leads": [
    {
      "name": "Acme Corp",
      "company_size": 500,
      "engagement_score": 85,
      "has_budget": true,
      "score": 78,
      "breakdown": { "size": 15, "engagement": 43, "budget": 20 }
    },
    {
      "name": "BigCo Ltd",
      "company_size": 5000,
      "engagement_score": 40,
      "has_budget": true,
      "score": 70,
      "breakdown": { "size": 30, "engagement": 20, "budget": 20 }
    },
    {
      "name": "MidRange LLC",
      "company_size": 200,
      "engagement_score": 70,
      "has_budget": true,
      "score": 61,
      "breakdown": { "size": 6, "engagement": 35, "budget": 20 }
    },
    {
      "name": "Startup Inc",
      "company_size": 15,
      "engagement_score": 92,
      "has_budget": false,
      "score": 47,
      "breakdown": { "size": 0, "engagement": 46, "budget": 0 }
    }
  ],
  "top_lead": "Acme Corp"
}
```

The `weights` variable uses Fixed mode, so you adjust the scoring without editing code. The `breakdown` object records how each criterion contributed to the final score.

## Generate a summary report with JavaScript

This example computes campaign metrics and produces both a readable report and structured data.

Declare the following input variable:

| **Name**           | **Type** | **Mode**  | **Description**                                                           |
| ------------------ | -------- | --------- | ------------------------------------------------------------------------- |
| `campaign_results` | Array    | Automatic | Campaign objects with `name`, `impressions`, `clicks`, and `conversions`. |

Enter the following sample data:

```json
{
  "campaign_results": [
    { "name": "Summer Sale", "impressions": 50000, "clicks": 2500, "conversions": 150 },
    { "name": "Product Launch", "impressions": 120000, "clicks": 8400, "conversions": 620 },
    { "name": "Retargeting Q3", "impressions": 30000, "clicks": 1800, "conversions": 210 },
    { "name": "Brand Awareness", "impressions": 200000, "clicks": 6000, "conversions": 90 }
  ]
}
```

Use the following JavaScript handler:

```javascript
async function handler(input, ctx) {
    const campaigns = input.campaign_results;

    const totalImpressions = campaigns.reduce((sum, c) => sum + c.impressions, 0);
    const totalClicks = campaigns.reduce((sum, c) => sum + c.clicks, 0);
    const totalConversions = campaigns.reduce((sum, c) => sum + c.conversions, 0);

    const avgCtr = totalImpressions > 0
        ? ((totalClicks / totalImpressions) * 100).toFixed(2) : "0.00";
    const avgConvRate = totalClicks > 0
        ? ((totalConversions / totalClicks) * 100).toFixed(2) : "0.00";

    const withRates = campaigns.map(c => ({
        ...c,
        ctr: c.impressions > 0 ? ((c.clicks / c.impressions) * 100).toFixed(2) : "0.00",
        conversion_rate: c.clicks > 0 ? ((c.conversions / c.clicks) * 100).toFixed(2) : "0.00"
    }));

    withRates.sort((a, b) => parseFloat(b.conversion_rate) - parseFloat(a.conversion_rate));
    const best = withRates[0];
    const worst = withRates[withRates.length - 1];

    return {
        report_text: `Campaign Performance Report\nTotal: ${campaigns.length} campaigns, ${avgCtr}% CTR, ${avgConvRate}% conv rate\nTop: ${best.name} (${best.conversion_rate}%)\nNeeds attention: ${worst.name} (${worst.conversion_rate}%)`,
        metrics: { totalImpressions, totalClicks, totalConversions, avgCtr, avgConvRate },
        campaign_details: withRates,
        best_campaign: best.name,
        worst_campaign: worst.name,
        summary: `Report for ${campaigns.length} campaigns: ${avgCtr}% CTR`
    };
}
```

The handler returns the following value:

```json
{
  "report_text": "Campaign Performance Report\nTotal: 4 campaigns, 4.68% CTR, 5.72% conv rate\nTop: Retargeting Q3 (11.67%)\nNeeds attention: Brand Awareness (1.50%)",
  "metrics": {
    "totalImpressions": 400000,
    "totalClicks": 18700,
    "totalConversions": 1070,
    "avgCtr": "4.68",
    "avgConvRate": "5.72"
  },
  "campaign_details": [
    { "name": "Retargeting Q3", "impressions": 30000, "clicks": 1800, "conversions": 210, "ctr": "6.00", "conversion_rate": "11.67" },
    { "name": "Product Launch", "impressions": 120000, "clicks": 8400, "conversions": 620, "ctr": "7.00", "conversion_rate": "7.38" },
    { "name": "Summer Sale", "impressions": 50000, "clicks": 2500, "conversions": 150, "ctr": "5.00", "conversion_rate": "6.00" },
    { "name": "Brand Awareness", "impressions": 200000, "clicks": 6000, "conversions": 90, "ctr": "3.00", "conversion_rate": "1.50" }
  ],
  "best_campaign": "Retargeting Q3",
  "worst_campaign": "Brand Awareness"
}
```

A downstream agent uses `report_text` as context for generating insights. Other steps branch on the values in `metrics`.

## Read an input file

This example reads a file uploaded to the conversation, parses it as comma-separated values, and returns summary data. It falls back to listing the available files when the one you named is absent.

Declare the following input variable:

| **Name**   | **Type** | **Mode**  | **Description**                           |
| ---------- | -------- | --------- | ----------------------------------------- |
| `filename` | String   | Automatic | The name of the uploaded file to process. |

Enter the following sample data:

```json
{
  "filename": "sales_q3.csv"
}
```

Use the following Python handler:

```python
import csv
import os

def handler(input, ctx):
    filename = input["filename"]
    attachments = os.path.join(ctx["input_dir"], "attachments")
    filepath = os.path.join(attachments, filename)

    if not os.path.isfile(filepath):
        available = sorted(os.listdir(attachments)) if os.path.isdir(attachments) else []
        return {
            "error": f"File not found: {filename}",
            "available_files": available,
            "summary": f"{filename} is not in the attachments directory"
        }

    with open(filepath, "r") as f:
        reader = csv.DictReader(f)
        records = list(reader)
        columns = reader.fieldnames or []

    return {
        "filename": filename,
        "row_count": len(records),
        "columns": columns,
        "first_rows": records[:3],
        "summary": f"Read {filename}: {len(records)} rows, {len(columns)} columns"
    }
```

Given a file with `region`, `sales`, and `target` columns, the handler returns the following value:

```json
{
  "filename": "sales_q3.csv",
  "row_count": 4,
  "columns": ["region", "sales", "target"],
  "first_rows": [
    { "region": "North", "sales": "45000", "target": "50000" },
    { "region": "South", "sales": "62000", "target": "55000" },
    { "region": "East", "sales": "38000", "target": "40000" }
  ]
}
```

A test run has no input files. This example needs a full workflow execution with a file uploaded to Mark chat. The fallback branch reports what did arrive, which turns a missing file into readable output rather than a failed step.

`csv.DictReader` returns every value as a string, so `sales` arrives as `"45000"` rather than `45000`. The [Parse CSV text into structured data](docId\:MCizwEDmqQkfmrPWot8wi) example shows how to convert the numeric columns.

## Write output files

This example writes results to the output directory so downstream steps read them as files.

Declare the following input variables:

| **Name**   | **Type** | **Mode**  | **Description**           |
| ---------- | -------- | --------- | ------------------------- |
| `records`  | Array    | Automatic | Record objects to export. |
| `filename` | String   | Fixed     | Output file name.         |

Enter the following sample data:

```json
{
  "records": [
    { "id": 1, "name": "Alice", "score": 95 },
    { "id": 2, "name": "Bob", "score": 82 },
    { "id": 3, "name": "Charlie", "score": 91 }
  ],
  "filename": "results.csv"
}
```

Use the following Python handler:

```python
import csv
import json
import os

def handler(input, ctx):
    records = input["records"]
    filename = input["filename"]

    if not records:
        return {"file_path": None, "summary": "No records to export"}

    output_path = os.path.join(ctx["output_dir"], filename)
    columns = list(records[0].keys())

    with open(output_path, "w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=columns)
        writer.writeheader()
        writer.writerows(records)

    json_path = os.path.join(ctx["output_dir"], filename.replace(".csv", ".json"))
    with open(json_path, "w") as f:
        json.dump(records, f, indent=2)

    return {
        "csv_file": output_path,
        "json_file": json_path,
        "row_count": len(records),
        "columns": columns,
        "summary": f"Exported {len(records)} records to {filename}"
    }
```

The handler returns the following value:

```json
{
  "csv_file": "/workspace/output/results.csv",
  "json_file": "/workspace/output/results.json",
  "row_count": 3,
  "columns": ["id", "name", "score"]
}
```

The handler writes both files to the path in `ctx["output_dir"]`. Those files display in the **Output Files** section of the execution details panel.

## Write output files with Bash

This example uses Bash and `jq` to process JavaScript Object Notation (JSON) input and write files. File generation is the primary use case for a Bash code step, because a Bash step always returns `null`.

Declare the following input variable:

| **Name**      | **Type** | **Mode**  | **Description**                                                                     |
| ------------- | -------- | --------- | ----------------------------------------------------------------------------------- |
| `report_data` | Object   | Automatic | Object with a title and an array of items holding region, sales, and target fields. |

Enter the following sample data:

```json
{
  "report_data": {
    "title": "Weekly Sales Summary",
    "items": [
      { "region": "North", "sales": 45000, "target": 50000 },
      { "region": "South", "sales": 62000, "target": 55000 },
      { "region": "East", "sales": 38000, "target": 40000 },
      { "region": "West", "sales": 51000, "target": 48000 }
    ]
  }
}
```

Use the following Bash handler:

```bash
#!/usr/bin/env bash

TITLE=$(jq -r '.report_data.title' <<< "$INPUT_JSON")

echo "region,sales,target,variance" > "$OPAL_OUTPUT_DIR/report.csv"
jq -r '.report_data.items[] | "\(.region),\(.sales),\(.target),\(.sales - .target)"' \
 <<< "$INPUT_JSON" >> "$OPAL_OUTPUT_DIR/report.csv"

{
 echo "$TITLE"
 echo "========================"
 jq -r '.report_data.items[] |
   "\(.region): $\(.sales) / $\(.target) (variance: \(.sales - .target))"' \
   <<< "$INPUT_JSON"
 echo ""
 echo "Total sales: $(jq '[.report_data.items[].sales] | add' <<< "$INPUT_JSON")"
 echo "Total target: $(jq '[.report_data.items[].target] | add' <<< "$INPUT_JSON")"
} > "$OPAL_OUTPUT_DIR/summary.txt"

echo "Wrote report.csv and summary.txt to the output directory"
```

The script writes the following line to standard output and returns `null`:

```text
Wrote report.csv and summary.txt to the output directory
```

The script produces `report.csv` with the following contents:

```text
region,sales,target,variance
North,45000,50000,-5000
South,62000,55000,7000
East,38000,40000,-2000
West,51000,48000,3000
```

The script also produces `summary.txt` with the following contents:

```text
Weekly Sales Summary
========================
North: $45000 / $50000 (variance: -5000)
South: $62000 / $55000 (variance: 7000)
East: $38000 / $40000 (variance: -2000)
West: $51000 / $48000 (variance: 3000)

Total sales: 196000
Total target: 193000
```

Both files display in the **Output Files** section of the execution details panel. Downstream steps and agents reference them by path.

## Chain two code steps

Chain code steps so the output of one flows into the next. This splits complex logic into discrete stages that you test independently.

This example uses the following layout:

```text
[Agent: Gather survey data] -> [Code step: Clean and normalize]
 -> [Code step: Analyze and score] -> [Agent: Generate report]
```

### Clean and normalize the responses

Declare the following input variable:

| **Name**        | **Type** | **Mode**  | **Description**                               |
| --------------- | -------- | --------- | --------------------------------------------- |
| `raw_responses` | Array    | Automatic | Raw survey responses from the upstream agent. |

Enter the following sample data:

```json
{
  "raw_responses": [
    { "name": "  Alice ", "rating": "9", "comment": "Great product!", "timestamp": "2026-08-01T10:00:00Z" },
    { "name": "Bob", "rating": "3", "comment": " Needs improvement. ", "timestamp": "2026-08-01T11:30:00Z" },
    { "rating": "7", "comment": "Decent experience", "timestamp": "2026-08-02T09:00:00Z" },
    { "name": "Dana", "rating": "15", "comment": "Love it!", "timestamp": "2026-08-02T14:00:00Z" }
  ]
}
```

Use the following Python handler:

```python
def handler(input, ctx):
    responses = input["raw_responses"]

    cleaned = []
    for r in responses:
        cleaned.append({
            "respondent": r.get("name", "Anonymous").strip(),
            "rating": max(1, min(10, int(r.get("rating", 5)))),
            "comment": r.get("comment", "").strip(),
            "timestamp": r.get("timestamp", "")
        })

    return {
        "cleaned_responses": cleaned,
        "total_responses": len(cleaned),
        "summary": f"Cleaned {len(cleaned)} survey responses"
    }
```

The handler returns the following value:

```json
{
  "cleaned_responses": [
    { "respondent": "Alice", "rating": 9, "comment": "Great product!", "timestamp": "2026-08-01T10:00:00Z" },
    { "respondent": "Bob", "rating": 3, "comment": "Needs improvement.", "timestamp": "2026-08-01T11:30:00Z" },
    { "respondent": "Anonymous", "rating": 7, "comment": "Decent experience", "timestamp": "2026-08-02T09:00:00Z" },
    { "respondent": "Dana", "rating": 10, "comment": "Love it!", "timestamp": "2026-08-02T14:00:00Z" }
  ],
  "total_responses": 4
}
```

The handler trims whitespace from names and comments. It defaults a missing name to `Anonymous`, and clamps the out-of-range rating of 15 down to 10.

### Analyze and score the clean data

This step receives the output of the cleaning step. Declare its variable in Automatic mode so the orchestrator maps the upstream output onto it.

Declare the following input variable:

| **Name**            | **Type** | **Mode**  | **Description**                                                  |
| ------------------- | -------- | --------- | ---------------------------------------------------------------- |
| `cleaned_responses` | Array    | Automatic | The cleaned survey response objects from the previous code step. |

Enter the following sample data:

```json
{
  "cleaned_responses": [
    { "respondent": "Alice", "rating": 9, "comment": "Great product!", "timestamp": "2026-08-01T10:00:00Z" },
    { "respondent": "Bob", "rating": 3, "comment": "Needs improvement.", "timestamp": "2026-08-01T11:30:00Z" },
    { "respondent": "Anonymous", "rating": 7, "comment": "Decent experience", "timestamp": "2026-08-02T09:00:00Z" },
    { "respondent": "Dana", "rating": 10, "comment": "Love it!", "timestamp": "2026-08-02T14:00:00Z" }
  ]
}
```

Use the following Python handler:

```python
def handler(input, ctx):
    responses = input["cleaned_responses"]

    ratings = [r["rating"] for r in responses]
    avg_rating = sum(ratings) / len(ratings) if ratings else 0
    promoters = sum(1 for r in ratings if r >= 9)
    detractors = sum(1 for r in ratings if r <= 6)
    nps = round(((promoters - detractors) / len(ratings)) * 100) if ratings else 0

    return {
        "average_rating": round(avg_rating, 2),
        "nps_score": nps,
        "promoters": promoters,
        "detractors": detractors,
        "total": len(ratings),
        "summary": f"NPS: {nps}, average rating {round(avg_rating, 2)}"
    }
```

The handler returns the following value:

```json
{
  "average_rating": 7.25,
  "nps_score": 25,
  "promoters": 2,
  "detractors": 1,
  "total": 4
}
```

Each code step carries a single responsibility. The first normalizes messy input, and the second computes metrics on clean data. This separation lets you test each step with its own sample data. It also lets you change the scoring without touching the cleaning logic.

## Process each item in a loop

Place a code step inside a **Loop** to process one item at a time. A loop runs the steps inside it once for each item in a collection. A loop cannot start with a code step, so an agent step comes first. See [Workflow agent loops](docId\:Eh6uXq7qNIJ8AIbTU-e27) for more information. Declare an Automatic variable to receive the current item.

This example uses the following layout:

```text
[Loop: over products] -> [Agent: Look up competitor price]
       -> [Code step: Enrich product] -> [End loop]
```

Declare the following input variable:

| **Name**  | **Type** | **Mode**  | **Description**                                     |
| --------- | -------- | --------- | --------------------------------------------------- |
| `product` | Object   | Automatic | The current product object from the loop iteration. |

Enter sample data that represents a single iteration:

```json
{
  "product": {
    "name": "Widget Pro",
    "price": 49.99,
    "cost": 22.50,
    "units_sold": 1200,
    "category": "Hardware"
  }
}
```

Use the following Python handler:

```python
def handler(input, ctx):
    product = input["product"]

    price = product["price"]
    cost = product["cost"]
    units_sold = product["units_sold"]

    margin = price - cost
    margin_pct = round((margin / price) * 100, 1) if price > 0 else 0

    return {
        "name": product["name"],
        "category": product["category"],
        "price": price,
        "margin_pct": margin_pct,
        "revenue": round(price * units_sold, 2),
        "profit": round(margin * units_sold, 2),
        "summary": f"{product['name']}: {margin_pct}% margin"
    }
```

The handler returns the following value for one iteration:

```json
{
  "name": "Widget Pro",
  "category": "Hardware",
  "price": 49.99,
  "margin_pct": 55.0,
  "revenue": 59988.0,
  "profit": 32988.0
}
```

The loop collects the return value of each iteration. When every iteration finishes, downstream steps receive the collected results as an array.
