Map Transitions

Map Transitions – The Complete Guide to Token Transformation in Agentic Nets

The Map transition is the data transformation powerhouse in Agentic Nets. While Pass transitions only route tokens, Map transitions transform them through templates before routing. Think of it as a data pipeline: tokens arrive, get reshaped into a new structure, and are then sent to their destinations based on the transformed data.

In this comprehensive guide, we’ll explore every capability of Map transitions:

  • Template interpolation – building new token structures with ${...} expressions
  • Field mapping – renaming and restructuring data
  • Metadata access – including token IDs and names in output
  • The @response semantic – routing based on transformed data
  • Multi-emit patterns – sending transformed tokens to multiple places
  • Compound conditions – AND/OR logic on transformed fields
  • Numeric comparisons – threshold-based routing

Anatomy of a Map Transition Inscription

Every Map transition inscription follows this structure:

{
  "id": "t-my-transformer",
  "kind": "map",
  "mode": "SINGLE",

  "presets": {
    "input": {
      "placeId": "input-queue",
      "host": "myModel@localhost:8080",
      "arcql": "FROM $ LIMIT 1",
      "take": "FIRST",
      "consume": true
    }
  },

  "postsets": {
    "output": { "placeId": "output-queue", "host": "myModel@localhost:8080" }
  },

  "action": {
    "type": "map",
    "template": {
      "originalField": "${input.data.field}",
      "newField": "static-value",
      "tokenId": "${input._meta.id}"
    }
  },

  "emit": [
    { "to": "output", "from": "@response", "when": "newField == 'static-value'" }
  ]
}

Key differences from Pass transitions:

  • kind: "map" – identifies this as a Map transition
  • action.template – defines the transformation template
  • emit.from: "@response" – routes the transformed data (not @input.data)

Template Interpolation Syntax

Templates use ${...} expressions to access input token data:

Accessing User Data

User data is accessed via ${input.data.fieldName}:

// Input token
{ "orderId": "ORD-001", "amount": 150.50, "status": "pending" }

// Template
{
  "order": "${input.data.orderId}",
  "total": "${input.data.amount}",
  "originalStatus": "${input.data.status}"
}

// Result (@response)
{ "order": "ORD-001", "total": "150.50", "originalStatus": "pending" }

Accessing Token Metadata

Token metadata is accessed via ${input._meta.field}:

// Template with metadata
{
  "data": "${input.data.value}",
  "sourceTokenId": "${input._meta.id}",
  "sourceTokenName": "${input._meta.name}",
  "sourcePlaceId": "${input._meta.parentId}"
}

// Result includes token tracking info
{
  "data": "test-value",
  "sourceTokenId": "7f34a312-8cc2-4b1e-9a5f-...",
  "sourceTokenName": "token-abc123",
  "sourcePlaceId": "place-uuid-456"
}

Mixing Dynamic and Static Values

Templates can include both interpolated values and static constants:

// Template
{
  "orderId": "${input.data.orderId}",     // Dynamic - from input
  "status": "processed",                   // Static - always this value
  "processedBy": "map-transition",         // Static
  "processedAt": "2024-01-15T10:30:00Z"    // Static (could use external timestamp)
}

The @response Semantic

This is the critical difference between Map and Pass transitions:

Transition emit.from Condition Evaluates Against
Pass @input.data Original input token data
Map @response Transformed template result

Example: Routing by Transformed Status

Input token has no status field, but the template adds one:

// Input token (no status!)
{ "orderId": "ORD-001" }

// Template ADDS status
{
  "orderId": "${input.data.orderId}",
  "status": "processed"    // This is created by the template
}

// Emit rules evaluate @response (the transformed data)
"emit": [
  { "to": "processed", "from": "@response", "when": "status == 'processed'" },
  { "to": "pending", "from": "@response", "when": "status == 'pending'" }
]

// Result: Token routes to "processed" based on TRANSFORMED data!

This enables powerful patterns where the transformation itself determines the routing destination.

Pattern 1: Simple Field Mapping

Rename fields while preserving data:

{
  "id": "t-field-mapper",
  "kind": "map",
  "mode": "SINGLE",

  "presets": {
    "input": {
      "placeId": "raw-data",
      "host": "myModel@localhost:8080",
      "arcql": "FROM $ LIMIT 1",
      "take": "FIRST",
      "consume": true
    }
  },

  "postsets": {
    "output": { "placeId": "normalized-data", "host": "myModel@localhost:8080" }
  },

  "action": {
    "type": "map",
    "template": {
      "originalColor": "${input.data.color}",
      "originalSize": "${input.data.size}",
      "normalized": true
    }
  },

  "emit": [
    { "to": "output", "from": "@response" }
  ]
}

Token transformation:

// Input
{ "color": "blue", "size": "large" }

// Output (@response)
{ "originalColor": "blue", "originalSize": "large", "normalized": true }

Pattern 2: Multi-Emit with Transformed Data

Send the same transformed token to multiple destinations based on independent conditions:

{
  "id": "t-multi-router",
  "kind": "map",
  "mode": "SINGLE",

  "presets": {
    "input": {
      "placeId": "input",
      "host": "events@localhost:8080",
      "arcql": "FROM $ LIMIT 1",
      "take": "FIRST",
      "consume": true
    }
  },

  "postsets": {
    "orders": { "placeId": "orders-queue", "host": "events@localhost:8080" },
    "priority": { "placeId": "priority-queue", "host": "events@localhost:8080" },
    "processed": { "placeId": "processed-queue", "host": "events@localhost:8080" }
  },

  "action": {
    "type": "map",
    "template": {
      "type": "${input.data.type}",
      "priority": "${input.data.priority}",
      "status": "processed"
    }
  },

  "emit": [
    { "to": "orders", "from": "@response", "when": "type == 'order'" },
    { "to": "priority", "from": "@response", "when": "priority == 'high'" },
    { "to": "processed", "from": "@response", "when": "status == 'processed'" }
  ]
}

Token example:

// Input
{ "type": "order", "priority": "high" }

// Output goes to ALL THREE places:
// - orders-queue (type == 'order' ✓)
// - priority-queue (priority == 'high' ✓)
// - processed-queue (status == 'processed' ✓ from template)

Pattern 3: Compound AND Conditions

Require multiple conditions on transformed data to all be true:

{
  "action": {
    "type": "map",
    "template": {
      "level": "${input.data.level}",
      "score": "${input.data.score}",
      "grade": "A"
    }
  },

  "emit": [
    { "to": "elite", "from": "@response", "when": "grade == 'A' AND score > 90" }
  ]
}

Token example:

// Input
{ "level": 5, "score": 95 }

// Transformed (@response)
{ "level": "5", "score": "95", "grade": "A" }

// Condition: grade == 'A' (✓) AND score > 90 (✓)
// Result: Token emitted to "elite"

Pattern 4: Compound OR Conditions

Route when any one of multiple conditions on transformed data is true:

{
  "action": {
    "type": "map",
    "template": {
      "category": "${input.data.category}",
      "flagged": "false"
    }
  },

  "emit": [
    { "to": "attention", "from": "@response", "when": "category == 'urgent' OR flagged == true" }
  ]
}

Token example:

// Input
{ "category": "urgent" }

// Transformed (@response)
{ "category": "urgent", "flagged": "false" }

// Condition: category == 'urgent' (✓) OR flagged == true (✗)
// Result: Token emitted to "attention" (first condition satisfied)

Pattern 5: Numeric Comparisons

Route based on numeric thresholds applied to transformed data:

{
  "action": {
    "type": "map",
    "template": {
      "amount": "${input.data.rawAmount}",
      "tier": "gold"
    }
  },

  "postsets": {
    "high-value": { "placeId": "high-value-orders", "host": "orders@localhost:8080" },
    "low-value": { "placeId": "low-value-orders", "host": "orders@localhost:8080" }
  },

  "emit": [
    { "to": "high-value", "from": "@response", "when": "amount > 100" },
    { "to": "low-value", "from": "@response", "when": "amount <= 100" }
  ]
}

Type coercion: Numeric comparisons work even when values are stored as strings. The condition evaluator automatically converts "250" to 250 for numeric comparisons.

Pattern 6: Default Emit (No When Condition)

Omit the when clause to always emit transformed data:

{
  "action": {
    "type": "map",
    "template": {
      "transformed": "${input.data.data}",
      "timestamp": "2024-01-15T10:30:00Z"
    }
  },

  "emit": [
    { "to": "output", "from": "@response" }  // No "when" = always emits
  ]
}

Use cases:

  • Simple data normalization pipelines
  • Adding audit metadata to all tokens
  • Format conversion without conditional routing

Pattern 7: No-Match Behavior

When transformed data doesn’t match any emit condition, the input token is consumed but nothing is emitted:

// Input
{ "status": "unknown" }

// Template (preserves status)
{ "status": "${input.data.status}" }  // Result: { "status": "unknown" }

// Emit rules
{ "to": "active", "when": "status == 'active'" },
{ "to": "inactive", "when": "status == 'inactive'" }

// Result: Input consumed, NO output (status doesn't match any rule)

Tip: Add a catch-all emit rule if you want all tokens to go somewhere:

"emit": [
  { "to": "active", "from": "@response", "when": "status == 'active'" },
  { "to": "inactive", "from": "@response", "when": "status == 'inactive'" },
  { "to": "unknown", "from": "@response" }  // Catch-all (no condition)
]

Pattern 8: Boolean Field Transformation

Transform and route based on boolean flags:

{
  "action": {
    "type": "map",
    "template": {
      "isEnabled": "${input.data.enabled}",
      "status": "checked"
    }
  },

  "emit": [
    { "to": "enabled", "from": "@response", "when": "isEnabled == true" },
    { "to": "disabled", "from": "@response", "when": "isEnabled == false" }
  ]
}

Boolean coercion: String values "true" and "false" are automatically coerced to booleans for comparison.

Map vs Pass: When to Use Each

Use Case Pass Map
Simple routing without data changes
Filter/gate based on input data
Rename or restructure fields
Add new fields (timestamps, IDs, status)
Route based on computed/added values
Include token metadata in output
Normalize heterogeneous data

Combine Pass and Map in Pipelines

A powerful pattern is to use Pass for filtering, then Map for transformation:

Input → [Pass: Filter active only] → Active → [Map: Normalize + add metadata] → Processed

This keeps responsibilities clean:

  • Pass answers: “Should this token continue?”
  • Map answers: “What should this token look like?”

Complete Working Example

Here’s a complete Map transition that normalizes order data and routes by value tier:

{
  "id": "t-order-processor",
  "kind": "map",
  "mode": "SINGLE",

  "presets": {
    "input": {
      "placeId": "raw-orders",
      "host": "orders@localhost:8080",
      "arcql": "FROM $ LIMIT 1",
      "take": "FIRST",
      "consume": true
    }
  },

  "postsets": {
    "premium": { "placeId": "premium-orders", "host": "orders@localhost:8080" },
    "standard": { "placeId": "standard-orders", "host": "orders@localhost:8080" },
    "archive": { "placeId": "order-archive", "host": "orders@localhost:8080" }
  },

  "action": {
    "type": "map",
    "template": {
      "orderId": "${input.data.id}",
      "customerName": "${input.data.customer}",
      "amount": "${input.data.total}",
      "status": "processed",
      "processedAt": "2024-01-15T10:30:00Z",
      "sourceTokenId": "${input._meta.id}"
    }
  },

  "emit": [
    { "to": "premium", "from": "@response", "when": "amount > 1000" },
    { "to": "standard", "from": "@response", "when": "amount <= 1000" },
    { "to": "archive", "from": "@response" }
  ]
}

This transition:

  1. Reads one token from raw-orders
  2. Transforms it into a normalized structure with status and metadata
  3. Routes to premium-orders if amount > 1000
  4. Routes to standard-orders if amount <= 1000
  5. Always archives to order-archive (no condition)
  6. Consumes the original token from raw-orders

Best Practices

1. Use Hierarchical Paths Correctly

Always use the full path to access data:

  • ${input.data.field} for user data
  • ${input._meta.id} for token metadata

2. Remember: @response, Not @input.data

Map transitions should use @response in emit rules to route based on transformed data:

// Correct for Map
{ "to": "output", "from": "@response", "when": "status == 'processed'" }

// This would check ORIGINAL data, not transformed:
{ "to": "output", "from": "@input.data", "when": "status == 'processed'" }

3. Add Traceability with Metadata

Include source token info for debugging and audit trails:

{
  "businessField": "${input.data.value}",
  "_sourceTokenId": "${input._meta.id}",
  "_sourceTokenName": "${input._meta.name}",
  "_transformedBy": "t-my-transformer"
}

4. Use Default Emit for Archive/Audit

Always send to an archive place alongside conditional routes:

"emit": [
  { "to": "success", "from": "@response", "when": "status == 'success'" },
  { "to": "failure", "from": "@response", "when": "status == 'failure'" },
  { "to": "audit-log", "from": "@response" }  // Everything gets logged
]

Summary

Map transitions are the data transformation workhorses of Agentic Nets. With template interpolation, field mapping, metadata access, and conditional routing, you can build sophisticated data pipelines entirely through declarative JSON inscriptions.

Key takeaways:

  • Map transitions transform AND route – they reshape data before deciding destinations
  • Use @response for emit conditions – this evaluates the transformed data
  • Template syntax: ${input.data.field} – hierarchical access to token data
  • Metadata access: ${input._meta.id} – include source tracking
  • Multi-emit is independent – each emit rule is evaluated separately
  • Type coercion is automatic – strings, numbers, and booleans just work

Combined with Pass transitions for filtering and HTTP transitions for external integration, Map transitions form the backbone of powerful, maintainable agent orchestration in Agentic Nets.

Leave a Reply

Your email address will not be published. Required fields are marked *