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 transitionaction.template– defines the transformation templateemit.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:
- Reads one token from
raw-orders - Transforms it into a normalized structure with status and metadata
- Routes to
premium-ordersif amount > 1000 - Routes to
standard-ordersif amount <= 1000 - Always archives to
order-archive(no condition) - 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.