Pass Transitions

Pass Transitions – The Complete Guide to Token Routing in Agentic Nets

The Pass transition is the foundational routing primitive in Agentic Nets. It examines token data and decides where each token should go – without transforming it, without calling external systems, without side effects. Think of it as a railway switch operator: tokens arrive, get inspected, and are sent down the correct track.

In this comprehensive guide, we’ll explore every routing pattern the Pass transition supports:

  • Two-way routing – if/else branching based on field values
  • No-match behavior – what happens when no condition is satisfied
  • Compound AND conditions – require multiple criteria to match
  • Compound OR conditions – match any of several criteria
  • Numeric comparisons – greater than, less than, equals
  • Boolean conditions – true/false flag checking
  • Default routing – always-emit fallback patterns
  • Multi-emit – send the same token to multiple destinations

Anatomy of a Pass Transition Inscription

Every Pass transition inscription follows this structure:

{
  "id": "t-my-router",
  "kind": "task",
  "mode": "SINGLE",

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

  "postsets": {
    "outputA": { "placeId": "place-a", "host": "myModel@localhost:8080" },
    "outputB": { "placeId": "place-b", "host": "myModel@localhost:8080" }
  },

  "action": { "type": "pass" },

  "emit": [
    { "to": "outputA", "from": "@input.data", "when": "fieldName == 'value'" },
    { "to": "outputB", "from": "@input.data", "when": "fieldName != 'value'" }
  ]
}

Key components:

  • presets – where to read tokens from (with ArcQL query and consumption rules)
  • postsets – symbolic names for output destinations
  • action.type: "pass" – marks this as a pure routing transition
  • emit – array of routing rules with conditions

Pattern 1: Two-Way Routing

The most common use case: route tokens to one of two places based on a field value. This is the Petri net equivalent of an if/else statement.

Example: Order Status Router

Route incoming orders to either an “active” processing queue or an “inactive” archive based on their status:

{
  "id": "t-order-router",
  "kind": "task",
  "mode": "SINGLE",

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

  "postsets": {
    "active": { "placeId": "active-orders", "host": "orders@localhost:8080" },
    "inactive": { "placeId": "inactive-orders", "host": "orders@localhost:8080" }
  },

  "action": { "type": "pass" },

  "emit": [
    { "to": "active", "from": "@input.data", "when": "status == 'active'" },
    { "to": "inactive", "from": "@input.data", "when": "status == 'inactive'" }
  ]
}

Token example:

// Input token
{ "orderId": "ORD-001", "status": "active", "amount": 150.00 }

// Result: Token emitted to "active-orders" place

Behavior:

  • Token with status: "active" → goes to active-orders
  • Token with status: "inactive" → goes to inactive-orders
  • Input token is consumed (removed from input-queue)

Pattern 2: No-Match Behavior

What happens when a token doesn’t match any emit condition? The token is consumed but not emitted anywhere. This is useful for filtering out unwanted tokens.

Example: Filter Unknown Statuses

Using the same inscription as above, if a token arrives with status: "unknown":

// Input token
{ "orderId": "ORD-003", "status": "unknown" }

// Result: Token is CONSUMED but NOT EMITTED anywhere
// - input-queue: token removed
// - active-orders: empty
// - inactive-orders: empty

Use cases for no-match filtering:

  • Drop malformed or invalid tokens
  • Implement “strict mode” routing where only known values proceed
  • Silently discard test/debug tokens in production

Warning: If you want unknown values to go somewhere (like an error queue), add an explicit catch-all rule or use default routing (Pattern 7).

Pattern 3: Compound AND Conditions

Require multiple conditions to all be true before routing. Use the AND keyword between conditions.

Example: High-Priority Large Orders

Route orders to an “urgent” queue only if they are both high-priority AND have a large amount:

{
  "id": "t-urgent-filter",
  "kind": "task",
  "mode": "SINGLE",

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

  "postsets": {
    "urgent": { "placeId": "urgent-queue", "host": "orders@localhost:8080" }
  },

  "action": { "type": "pass" },

  "emit": [
    { "to": "urgent", "from": "@input.data", "when": "priority == 'high' AND amount > 1000" }
  ]
}

Token examples:

// Token 1: Both conditions true → EMITTED to urgent-queue
{ "priority": "high", "amount": 1500 }

// Token 2: Only one condition true → NOT EMITTED (consumed but dropped)
{ "priority": "high", "amount": 500 }

// Token 3: Both conditions false → NOT EMITTED
{ "priority": "low", "amount": 500 }

AND condition rules:

  • ALL conditions must be true for the emit to fire
  • Evaluation is left-to-right (but all are checked)
  • Mix string equality with numeric comparisons freely

Pattern 4: Compound OR Conditions

Route when any one of multiple conditions is true. Use the OR keyword.

Example: Priority or Active Status

Route tokens to a “fast-track” queue if they are either active OR high-priority:

{
  "id": "t-fast-track",
  "kind": "task",
  "mode": "SINGLE",

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

  "postsets": {
    "fast": { "placeId": "fast-track", "host": "tasks@localhost:8080" }
  },

  "action": { "type": "pass" },

  "emit": [
    { "to": "fast", "from": "@input.data", "when": "status == 'active' OR priority == 'high'" }
  ]
}

Token examples:

// Token 1: First condition true → EMITTED
{ "status": "active", "priority": "low" }

// Token 2: Second condition true → EMITTED
{ "status": "inactive", "priority": "high" }

// Token 3: Both true → EMITTED (still just once)
{ "status": "active", "priority": "high" }

// Token 4: Neither true → NOT EMITTED
{ "status": "inactive", "priority": "low" }

Pattern 5: Numeric Comparisons

Pass transitions support full numeric comparison operators: >, <, >=, <=, ==, !=

Example: Amount-Based Routing

Route orders to different queues based on their monetary value:

{
  "id": "t-amount-router",
  "kind": "task",
  "mode": "SINGLE",

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

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

  "action": { "type": "pass" },

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

Token examples:

// Token with amount 150.50 → high-value-orders
{ "orderId": "ORD-001", "amount": 150.50 }

// Token with amount 50.00 → low-value-orders
{ "orderId": "ORD-002", "amount": 50.00 }

// Token with amount exactly 100 → low-value-orders (due to <=)
{ "orderId": "ORD-003", "amount": 100 }

Supported numeric operators:

Operator Meaning Example
> Greater than amount > 100
< Less than amount < 50
>= Greater or equal amount >= 100
<= Less or equal amount <= 100
== Equals amount == 100
!= Not equals amount != 0

Type coercion: String values like "150.50" are automatically parsed as numbers when compared with numeric operators. This ensures tokens created via different APIs (some store numbers as strings) work seamlessly.

Pattern 6: Boolean Conditions

Check true/false flags in token data. Compare against literal true or false values.

Example: Urgency Flag Router

{
  "id": "t-urgency-router",
  "kind": "task",
  "mode": "SINGLE",

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

  "postsets": {
    "urgent": { "placeId": "urgent-tasks", "host": "tasks@localhost:8080" },
    "normal": { "placeId": "normal-tasks", "host": "tasks@localhost:8080" }
  },

  "action": { "type": "pass" },

  "emit": [
    { "to": "urgent", "from": "@input.data", "when": "isUrgent == true" },
    { "to": "normal", "from": "@input.data", "when": "isUrgent == false" }
  ]
}

Token examples:

// Token with isUrgent: true → urgent-tasks
{ "task": "review", "isUrgent": true }

// Token with isUrgent: false → normal-tasks
{ "task": "cleanup", "isUrgent": false }

Boolean handling notes:

  • String values "true" and "false" are coerced to booleans (case-insensitive)
  • Use == true or == false explicitly for clarity
  • Missing fields are treated as falsy (no match for == true)

Pattern 7: Default Routing (No Condition)

Omit the when clause to create an always-emit rule. This is perfect for catch-all routing or simple pass-through scenarios.

Example: Simple Pass-Through

{
  "id": "t-pass-through",
  "kind": "task",
  "mode": "SINGLE",

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

  "postsets": {
    "output": { "placeId": "output", "host": "pipeline@localhost:8080" }
  },

  "action": { "type": "pass" },

  "emit": [
    { "to": "output", "from": "@input.data" }
  ]
}

Behavior: Every token that enters input is unconditionally forwarded to output.

Example: Catch-All Error Route

Combine conditional routes with a default fallback:

"emit": [
  { "to": "active", "from": "@input.data", "when": "status == 'active'" },
  { "to": "pending", "from": "@input.data", "when": "status == 'pending'" },
  { "to": "error", "from": "@input.data" }
]

Important: Emit rules are evaluated independently – the default rule fires for ALL tokens. If you want true “else” behavior, use explicit conditions like status != 'active' AND status != 'pending'.

Pattern 8: Multi-Emit (Same Token, Multiple Destinations)

Unlike traditional if/else, Pass transitions can emit the same token to multiple places when multiple conditions match. Each emit rule is evaluated independently.

Example: Tag-Based Fanout

Route a token to both an “orders” queue and a “priority” queue if it matches both criteria:

{
  "id": "t-multi-router",
  "kind": "task",
  "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" }
  },

  "action": { "type": "pass" },

  "emit": [
    { "to": "orders", "from": "@input.data", "when": "type == 'order'" },
    { "to": "priority", "from": "@input.data", "when": "priority == 'high'" }
  ]
}

Token example:

// Input token
{ "type": "order", "priority": "high", "orderId": "ORD-001" }

// Result: Token emitted to BOTH places!
// - orders-queue: receives the token
// - priority-queue: also receives the token
// - input: token consumed (removed)

Multi-emit use cases:

  • Event broadcasting – send the same event to multiple handlers
  • Audit trails – route to both processing queue AND audit log
  • Parallel processing – fan out to multiple downstream agentic processes
  • Tagging systems – route based on multiple independent criteria

Understanding the Emit Semantic

The emit array has a specific semantic that’s important to understand:

The from Expression

For Pass transitions, from specifies what data to emit. The most common value is @input.data which sends the token’s business payload unchanged.

  • @input.data – the token’s data properties (what you stored)
  • @input._meta – token metadata (id, name, parentId)
  • @input – the entire token structure (data + _meta)

The when Condition

The when condition is evaluated against what from resolves to. For @input.data, this means field names are accessed directly:

// Token data: { "status": "active", "amount": 100 }
// Emit rule: { "from": "@input.data", "when": "status == 'active'" }

// "status" refers to the field in the resolved data
// NOT @input.data.status - just "status"

Complete Condition Syntax Reference

Pattern Example Description
String equality status == 'active' Field equals string value
String inequality status != 'active' Field not equal to string
Numeric greater amount > 100 Field greater than number
Numeric less amount < 50 Field less than number
Numeric equal count == 10 Field equals number
Boolean true isUrgent == true Field is true
Boolean false isEnabled == false Field is false
AND compound a == 'x' AND b > 10 Both conditions must match
OR compound a == 'x' OR b == 'y' Either condition matches
No condition (omit when) Always emits

Best Practices

1. Use Mutually Exclusive Conditions for Single-Destination Routing

If you want exactly one destination per token, ensure your conditions don’t overlap:

// Good: mutually exclusive
{ "when": "status == 'active'" }
{ "when": "status == 'inactive'" }
{ "when": "status != 'active' AND status != 'inactive'" }  // catch-all

// Risky: might emit to both if status is 'active'
{ "when": "status == 'active'" }
{ "when": "priority == 'high'" }  // Could also match!

2. Handle Edge Cases Explicitly

Don’t rely on no-match behavior for important tokens. Add explicit error/unknown routes:

"emit": [
  { "to": "success", "from": "@input.data", "when": "status == 'complete'" },
  { "to": "retry", "from": "@input.data", "when": "status == 'pending'" },
  { "to": "dead-letter", "from": "@input.data", "when": "status != 'complete' AND status != 'pending'" }
]

3. Keep Pass Transitions Simple

Pass transitions should only route – no transformation. If you need to modify the token, use a Map transition instead:

  • Pass: “Where should this token go?”
  • Map: “What should this token look like?”

4. Use Descriptive Place Names

Your emit rules become self-documenting when postset names are clear:

// Clear intent
{ "to": "approved-orders", "when": "status == 'approved'" }
{ "to": "rejected-orders", "when": "status == 'rejected'" }
{ "to": "pending-review", "when": "status == 'pending'" }

// Unclear
{ "to": "output1", "when": "status == 'approved'" }
{ "to": "output2", "when": "status == 'rejected'" }

Summary

The Pass transition is deceptively simple but incredibly powerful. With just a few building blocks – string comparisons, numeric operators, boolean checks, AND/OR compounds, and multi-emit – you can implement sophisticated routing logic without writing code.

Key takeaways:

  • Pass transitions route without transforming – pure decision logic
  • Each emit rule is independent – tokens can go to multiple places
  • No-match means consumed but not emitted – useful for filtering
  • Omit “when” for always-emit behavior – catch-alls and pass-throughs
  • Type coercion handles strings vs numbers vs booleans – just works

In the next article, we’ll explore Map transitions for data transformation, and how Pass and Map work together to build clean, maintainable agentic processes.

Leave a Reply

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