Data Mapping

Master input and output mapping between workflow steps using template syntax, nested property access, and data transformations.

Overview

Data mapping enables information flow through workflows. Each step can reference outputs from previous steps, chain inputs, or static values using template syntax.

The chain maintains a context object that accumulates all outputs as steps execute. Any step can access any data in this context.

Template Syntax

Use double curly braces {{key}} to reference values from the chain context:

{
  "inputMapping": {
    "userMessage": "{{message}}",
    "sentiment": "{{sentiment_analysis.sentiment}}",
    "confidence": "{{sentiment_analysis.confidence}}"
  }
}

What Can Be Referenced

  • Chain inputs - Any input provided when the chain was executed
  • Step outputs - Outputs from any previous step (accessed via outputKey)
  • Nested properties - Use dot notation to access nested object properties
  • Array elements - Use bracket notation to access array elements

Context Building

As the chain executes, the context object grows with each step's output:

Context Evolution Example
json
// Initial chain input
{
  "userMessage": "I'm very unhappy with my order",
  "userId": "user_123"
}

// After step 0 (sentiment analysis) with outputKey "sentiment"
{
  "userMessage": "I'm very unhappy with my order",
  "userId": "user_123",
  "sentiment": {
    "sentiment": "Negative",
    "confidence": 0.92,
    "keyPhrases": ["very unhappy", "order"]
  }
}

// After step 1 (user lookup) with outputKey "user"
{
  "userMessage": "I'm very unhappy with my order",
  "userId": "user_123",
  "sentiment": { ... },
  "user": {
    "id": "user_123",
    "name": "John Doe",
    "email": "john@example.com",
    "tier": "premium"
  }
}

// After step 2 (generate response) with outputKey "response"
{
  "userMessage": "I'm very unhappy with my order",
  "userId": "user_123",
  "sentiment": { ... },
  "user": { ... },
  "response": "Dear John, I sincerely apologize for your experience..."
}
Every subsequent step has access to all previous outputs. The context is read-only - steps can only add new keys via their outputKey, not modify existing ones.

Nested Property Access

Use dot notation to access nested object properties:

{
  "inputMapping": {
    "userName": "{{user.name}}",
    "userEmail": "{{user.email}}",
    "sentimentScore": "{{sentiment.confidence}}",
    "isHighPriority": "{{user.tier}}",
    "deepProperty": "{{response.data.analysis.summary}}"
  }
}

Array Access

{
  "inputMapping": {
    "firstItem": "{{items[0]}}",
    "secondItemName": "{{items[1].name}}",
    "lastPurchaseDate": "{{purchases[-1].date}}",
    "nestedArray": "{{data.orders[0].items[2].sku}}"
  }
}
Accessing non-existent properties or array indices will result in null. Ensure data exists before referencing nested properties.

Input Mapping Patterns

Direct Mapping

{
  "inputMapping": {
    "text": "{{userMessage}}"
  }
}

Renaming

{
  "inputMapping": {
    "customerMessage": "{{userMessage}}",
    "customerName": "{{user.name}}"
  }
}

Combining Multiple Sources

{
  "inputMapping": {
    "message": "{{userMessage}}",
    "sentiment": "{{sentiment_analysis.sentiment}}",
    "confidence": "{{sentiment_analysis.confidence}}",
    "userName": "{{user.name}}",
    "userTier": "{{user.tier}}",
    "context": "{{previousResponse}}"
  }
}

Passing Entire Objects

{
  "inputMapping": {
    "user": "{{user}}",
    "sentimentData": "{{sentiment_analysis}}",
    "orderDetails": "{{api_response.data}}"
  }
}

Static Values

{
  "inputMapping": {
    "message": "{{userMessage}}",
    "language": "en",
    "maxLength": 500,
    "includeReasoning": true
  }
}
You can mix template variables and static values in the same inputMapping. Static values don't use the {{...}} syntax.

Output Keys

The outputKey determines how a step's output is stored in the context:

{
  "order": 0,
  "type": "PROMPT",
  "promptId": "sentiment-analyzer",
  "inputMapping": { "text": "{{userMessage}}" },
  "outputKey": "sentiment_analysis"
}

This step's output will be accessible as {{sentiment_analysis}} in subsequent steps.

Naming Conventions

  • Use snake_case - sentiment_analysis, user_data, api_response
  • Be descriptive - What does this output contain?
  • Avoid generic names - Don't use "result", "output", "data"
  • Indicate the content - sentiment_analysis, order_details, recommendations

Overwriting Output Keys

[
  {
    "order": 2,
    "type": "PROMPT",
    "promptId": "apology-response",
    "tags": ["negative_path"],
    "outputKey": "response"
  },
  {
    "order": 3,
    "type": "PROMPT",
    "promptId": "friendly-response",
    "tags": ["positive_path"],
    "outputKey": "response"
  }
]

Multiple steps can use the same outputKey. The last step to execute (based on conditional logic) will set the final value. This is useful for conditional branches that should all produce the same output type.

API Call Mapping

API call steps support template variables in URLs, headers, query parameters, and request bodies:

Complete API Call Mapping
json
{
  "order": 1,
  "type": "API_CALL",
  "config": {
    "url": "https://api.example.com/users/{{userId}}/orders",
    "method": "POST",
    "headers": {
      "Authorization": "Bearer {{apiKey}}",
      "Content-Type": "application/json",
      "X-User-Tier": "{{user.tier}}"
    },
    "queryParams": {
      "limit": "10",
      "sentiment": "{{sentiment_analysis.sentiment}}"
    },
    "body": {
      "userId": "{{userId}}",
      "message": "{{userMessage}}",
      "sentiment": {
        "value": "{{sentiment_analysis.sentiment}}",
        "confidence": "{{sentiment_analysis.confidence}}"
      },
      "timestamp": "{{now}}"
    }
  },
  "outputKey": "api_response"
}

Response Data Access

API responses are automatically parsed as JSON and stored with the output key. Access response data using dot notation:

// API response stored in "api_response":
{
  "status": "success",
  "data": {
    "ticketId": "TKT-12345",
    "assignedTo": "support-team",
    "priority": "high"
  },
  "meta": {
    "processingTime": 150
  }
}

// Reference in next step:
{
  "inputMapping": {
    "ticketId": "{{api_response.data.ticketId}}",
    "assignee": "{{api_response.data.assignedTo}}",
    "priority": "{{api_response.data.priority}}"
  }
}

Transform Steps for Data Manipulation

Use TRANSFORM steps when simple template mapping isn't enough:

Merging Multiple Objects

{
  "order": 5,
  "type": "TRANSFORM",
  "config": {
    "operation": "merge",
    "inputs": [
      "{{user}}",
      "{{sentiment_analysis}}",
      "{{api_response.data}}"
    ]
  },
  "outputKey": "enriched_data"
}

// Result stored in "enriched_data":
{
  "id": "user_123",
  "name": "John Doe",
  "email": "john@example.com",
  "sentiment": "Negative",
  "confidence": 0.92,
  "ticketId": "TKT-12345"
}

Extracting Specific Fields

{
  "order": 2,
  "type": "TRANSFORM",
  "config": {
    "operation": "extract",
    "input": "{{api_response}}",
    "fields": ["data.userId", "data.email", "status", "meta.processingTime"]
  },
  "outputKey": "extracted_data"
}

// Result:
{
  "userId": "user_123",
  "email": "john@example.com",
  "status": "success",
  "processingTime": 150
}

String Formatting

{
  "order": 3,
  "type": "TRANSFORM",
  "config": {
    "operation": "format",
    "template": "User {{user.name}} ({{user.email}}) - Sentiment: {{sentiment.sentiment}} ({{sentiment.confidence}})",
    "inputs": {
      "user": "{{user}}",
      "sentiment": "{{sentiment_analysis}}"
    }
  },
  "outputKey": "formatted_summary"
}

// Result:
"User John Doe (john@example.com) - Sentiment: Negative (0.92)"

Calculations

{
  "order": 4,
  "type": "TRANSFORM",
  "config": {
    "operation": "calculate",
    "expression": "({{order.subtotal}} + {{order.tax}}) * (1 - {{user.discountRate}})",
    "inputs": {
      "order": "{{api_response.data}}",
      "user": "{{user}}"
    }
  },
  "outputKey": "final_total"
}

Common Patterns

Passing Context Through Workflows

Build Context Progressively
json
[
  {
    "order": 0,
    "type": "API_CALL",
    "config": {
      "url": "https://api.example.com/users/{{userId}}"
    },
    "outputKey": "user"
  },
  {
    "order": 1,
    "type": "PROMPT",
    "promptId": "analyze-sentiment",
    "inputMapping": {
      "text": "{{userMessage}}"
    },
    "outputKey": "sentiment"
  },
  {
    "order": 2,
    "type": "TRANSFORM",
    "config": {
      "operation": "merge",
      "inputs": ["{{user}}", "{{sentiment}}"]
    },
    "outputKey": "context"
  },
  {
    "order": 3,
    "type": "PROMPT",
    "promptId": "generate-response",
    "inputMapping": {
      "message": "{{userMessage}}",
      "context": "{{context}}"
    },
    "outputKey": "response"
  }
]

Conditional Data Selection

[
  {
    "order": 0,
    "type": "CONDITION",
    "config": {
      "condition": "{{user.tier}} == 'premium'",
      "trueTag": "premium",
      "falseTag": "standard"
    }
  },
  {
    "order": 1,
    "type": "API_CALL",
    "tags": ["premium"],
    "config": {
      "url": "https://api.example.com/premium-features/{{userId}}"
    },
    "outputKey": "features"
  },
  {
    "order": 2,
    "type": "API_CALL",
    "tags": ["standard"],
    "config": {
      "url": "https://api.example.com/standard-features/{{userId}}"
    },
    "outputKey": "features"
  },
  {
    "order": 3,
    "type": "PROMPT",
    "promptId": "generate-response",
    "inputMapping": {
      "features": "{{features}}",
      "user": "{{user}}"
    },
    "outputKey": "response"
  }
]

Both branches write to the same features key, so step 3 can always reference {{features}} regardless of which branch executed.

Error Handling

Missing Data

// If a referenced key doesn't exist:
{
  "inputMapping": {
    "name": "{{user.name}}",
    "nonexistent": "{{does_not_exist}}"
  }
}

// Results in:
{
  "name": "John Doe",
  "nonexistent": null
}
Referencing non-existent keys returns null rather than causing an error. Validate data exists before using it in critical operations.

Safe Navigation

// Instead of:
"{{api_response.data.user.profile.avatar}}"

// Use a CONDITION step to check existence first:
{
  "order": 1,
  "type": "CONDITION",
  "config": {
    "condition": "{{api_response.data}} != null",
    "trueTag": "has_data",
    "falseTag": "no_data"
  }
}

Best Practices

Use Descriptive Output Keys - Name keys based on content, not step numbers. "sentiment_analysis" is better than "step1".
Document Data Structures - In chain descriptions, document what data each outputKey contains for easier maintenance.
Minimize Nesting - If accessing deeply nested properties, consider using a TRANSFORM step to extract and flatten the data first.
Validate Critical Data - Use CONDITION steps to check that required data exists before using it in subsequent steps.
Don't Reference Future Steps - You can only reference outputs from steps that have already executed (lower order numbers).

Next Steps

Workflow Examples

See complete workflows with data mapping

View Examples →

Chains API

Build chains programmatically via API

Chains API →