Tag-based flow control mechanisms allowing you to conditionally execute steps based on environments or feature flags.
Control logic is implemented via tags appended to pipeline steps.
Use @env to restrict steps to specific environments (defined by the WEBPIPE_ENV or equivalent configuration).
@env(production): Execute only if environment isproduction.@!env(dev): Execute only if environment is NOTdev.
GET /api/data
|> log: "Debug trace..." @env(dev)
|> pipeline: secureHeaders @env(production)
|> pg: `SELECT * FROM data`
Use @flag to toggle steps based on dynamic feature flags.
@flag(new-feature): Execute only ifnew-featureflag is enabled.@!flag(legacy-mode): Execute only iflegacy-modeflag is disabled.@flag(beta,staff): Execute only if ALL specified flags are enabled (multiple flags).
GET /checkout
|> pipeline: stripeProcessor @flag(use-stripe)
|> pipeline: paypalProcessor @!flag(use-stripe)
|> pipeline: betaFeatures @flag(beta,staff)
You can define a special featureFlags pipeline in your .wp files to dynamically calculate flag states for every request. This pipeline runs before the main route handler.
The pipeline should return a JSON object with a flags map at the root level.
pipeline featureFlags =
|> jq: `{
flags: {
"use-stripe": (.headers["x-beta-tester"] == "true"),
"new-ui": true
}
}`
# Flags are now available for @flag checks
Alternatively, you can use Lua to set flags dynamically via setFlag():
pipeline featureFlags =
|> lua: ```
local isBeta = input.headers and input.headers["x-beta-tester"] == "true"
setFlag("use-stripe", isBeta)
setFlag("new-ui", true)
return input
## If/Else Blocks
Use `if/else` blocks for conditional branching based on runtime data evaluation. Unlike tags which use static configuration, if/else blocks evaluate conditions dynamically during request execution.
### Basic Syntax
```wp
|> if
|> <condition-pipeline>
then:
|> <then-branch-steps>
else:
|> <else-branch-steps>
end
The else: and end keywords are optional.
- Condition Evaluation: The condition pipeline runs on a cloned copy of the current state
- Truthiness Check: The result is evaluated for truthiness:
falseandnullare falsy- Everything else is truthy (numbers, strings, objects, arrays, etc.)
- Branch Execution: The appropriate branch executes on the original state (not the condition output)
GET /api/users/:id
|> pg: `SELECT * FROM users WHERE id = $1`
|> if
|> jq: `.data.rows | length > 0`
then:
|> jq: `.data.rows[0]`
else:
|> jq: `{ error: "User not found" }`
If you omit the else: branch, the state passes through unchanged when the condition is false:
GET /api/data
|> jq: `{ value: 10 }`
|> if
|> jq: `.value > 5`
then:
|> jq: `. + { bonus: "high value" }`
# If condition is false, the original state continues
You can nest if blocks for complex logic:
GET /api/classify/:score
|> jq: `{ score: (.params.score | tonumber) }`
|> if
|> jq: `.score > 0`
then:
|> if
|> jq: `.score > 80`
then:
|> jq: `. + { grade: "A", tier: "premium" }`
else:
|> jq: `. + { grade: "B", tier: "standard" }`
else:
|> jq: `. + { grade: "F", tier: "free" }`
Conditions can include multiple pipeline steps:
GET /api/protected
|> if
|> jq: `{ sqlParams: [.headers.userId] }`
|> pg: `SELECT active FROM users WHERE id = $1`
|> jq: `.data.rows[0].active`
then:
|> jq: `{ message: "Access granted" }`
else:
|> jq: `{ error: "Access denied", status: 403 }`
When you have steps after an if/else block, use the end keyword to explicitly mark where the block ends:
GET /api/process/:value
|> jq: `{ value: (.params.value | tonumber) }`
|> if
|> jq: `.value > 50`
then:
|> jq: `{ category: "high" }`
else:
|> jq: `{ category: "low" }`
end
|> jq: `{ success: true, data: . }`
Without end, the parser might include subsequent steps as part of the else branch.
# These are FALSY (else branch executes):
|> if
|> jq: `false` # boolean false
|> if
|> jq: `null` # null value
# These are TRUTHY (then branch executes):
|> if
|> jq: `true` # boolean true
|> if
|> jq: `0` # number (even zero!)
|> if
|> jq: `""` # string (even empty!)
|> if
|> jq: `[]` # array (even empty!)
|> if
|> jq: `{}` # object (even empty!)
| Feature | Tags (@flag, @env) |
If/Else Blocks |
|---|---|---|
| Evaluation | Static (at route load) | Dynamic (at runtime) |
| Based on | Environment/flags | Request data |
| Granularity | Per-step | Per-pipeline segment |
| Use case | Environment toggling | Data-driven logic |
Dispatch blocks provide switch-like conditional branching based on tags. Unlike if/else blocks which evaluate runtime data, dispatch evaluates tags (@flag, @env, @when, @guard) to route execution through different branches.
|> dispatch
case @flag(feature-name):
|> <steps-when-flag-enabled>
case @env(production):
|> <steps-for-production>
default:
|> <fallback-steps>
The default: branch is optional but recommended for handling cases where no conditions match.
Route based on feature flags:
GET /feature-test-dispatch
|> dispatch
case @flag(experimental-feature):
|> jq: `{ message: "Experimental feature enabled!" }`
default:
|> jq: `{ message: "Standard feature (no flag required)" }`
Different behavior per environment:
GET /env-dispatch
|> dispatch
case @env(production):
|> jq: `{ env: "production", debug: false, caching: true }`
case @env(staging):
|> jq: `{ env: "staging", debug: true, caching: true }`
case @env(development):
|> jq: `{ env: "development", debug: true, caching: false }`
default:
|> jq: `{ env: "unknown", debug: false, caching: false }`
Use @! to check for absence:
GET /non-production-dispatch
|> dispatch
case @!env(production):
|> jq: `{
message: "Non-production environment",
debugTools: true,
mockData: true
}`
default:
|> jq: `{
message: "Production environment",
debugTools: false,
mockData: false
}`
Each case can contain multiple pipeline steps:
GET /multi-step-dispatch
|> dispatch
case @flag(experimental):
|> jq: `{ step: 1, version: "experimental" }`
|> jq: `. + { step: 2, features: ["ai", "realtime"] }`
|> jq: `. + { step: 3, complete: true }`
case @flag(beta):
|> jq: `{ step: 1, version: "beta" }`
|> jq: `. + { step: 2, features: ["analytics"] }`
default:
|> jq: `{ step: 1, version: "stable", features: [] }`
Combine multiple tags with and / or:
GET /test-dispatch-and
|> dispatch
case @when(authenticated) and @when(admin):
|> jq: `{ access: "admin_panel" }`
case @when(authenticated):
|> jq: `{ access: "user_dashboard" }`
default:
|> jq: `{ access: "login_required" }`
GET /test-dispatch-or
|> dispatch
case @when(mobile) or @when(tablet):
|> jq: `{ layout: "compact" }`
default:
|> jq: `{ layout: "desktop" }`
Mix different tag types:
GET /test-dispatch-mixed
|> dispatch
case @when(premium_user) and @!flag(maintenance):
|> jq: `{ access: "premium" }`
default:
|> jq: `{ access: "basic" }`
Explicit grouping with parentheses:
GET /test-dispatch-grouping
|> dispatch
case (@when(admin) or @when(staff)) and @when(on_duty):
|> jq: `{ status: "authorized" }`
default:
|> jq: `{ status: "unauthorized" }`
Precedence Rules:
andbinds tighter thanor@when(a) or @when(b) and @when(c)is evaluated as@when(a) or (@when(b) and @when(c))- Use parentheses for explicit grouping:
(@when(a) or @when(b)) and @when(c)
Foreach loops transform each element in an array in-place. The syntax allows you to apply a pipeline of transformations to every item in a specified array.
|> foreach <selector>
|> <transformation-steps>
end
The <selector> is a JQ-style path to the array (e.g., data.rows, items, users).
Transform each team name to uppercase:
GET /test-foreach-basic
|> jq: `{
data: {
rows: [
{ id: 1, name: "Alpha Team" },
{ id: 2, name: "Beta Team" },
{ id: 3, name: "Gamma Team" }
]
}
}`
|> foreach data.rows
|> jq: `. + { processed: true, uppercase_name: (.name | ascii_upcase) }`
end
|> jq: `{ teams: .data.rows }`
Result:
{
"teams": [
{ "id": 1, "name": "Alpha Team", "processed": true, "uppercase_name": "ALPHA TEAM" },
{ "id": 2, "name": "Beta Team", "processed": true, "uppercase_name": "BETA TEAM" },
{ "id": 3, "name": "Gamma Team", "processed": true, "uppercase_name": "GAMMA TEAM" }
]
}- The selector identifies the array to iterate over
- Each element is processed through the inner pipeline steps
- The transformed element replaces the original in the array
- The modified array remains at the same path in the request state
Batch Data Enrichment:
|> foreach users
|> jq: `. + { fullName: .firstName + " " + .lastName }`
|> jq: `. + { age: ((now - .birthdate) / 31536000) | floor }`
end
Data Normalization:
|> foreach products
|> jq: `. + { price: (.price | tonumber), inStock: (.quantity > 0) }`
end
Multi-Step Transformations:
|> foreach items
|> jq: `. + { priceWithTax: .price * 1.2 }`
|> jq: `. + { discount: (if .priceWithTax > 100 then 0.1 else 0) }`
|> jq: `. + { finalPrice: .priceWithTax * (1 - .discount) }`
end
The @guard tag enables conditional step execution based on JQ expressions evaluated against the current request state. Steps with guards are only executed if their expression evaluates to truthy.
@guard(\jq_expression`)`: Execute step only if expression is truthy@!guard(\jq_expression`)`: Execute step only if expression is falsy
|> jq: `. + { message: "Admin only" }` @guard(`.user.role == "admin"`)
|> jq: `. + { debugInfo: "..." }` @!guard(`.env == "production"`)
Execute steps based on user roles:
GET /admin/dashboard
|> jq: `{ user: { role: "admin", name: "Alice" } }`
|> jq: `. + { message: "Admin dashboard - restricted access" }` @guard(`.user.role == "admin"`)
|> jq: `. + { message: "Access denied" }` @guard(`.user.role != "admin"`)
Skip steps in certain environments:
GET /debug/info
|> jq: `{ env: "development", debug: true }`
|> log: `level: debug` @!guard(`.env == "production"`)
|> jq: `{ debugInfo: "Verbose logging enabled" }` @guard(`.debug == true`)
Combine multiple checks with and / or:
GET /premium/features
|> jq: `{
user: {
premium: true,
verified: true,
credits: 100
}
}`
|> jq: `. + { premiumData: "loaded" }` @guard(`.user.premium and .user.verified`)
|> jq: `. + { bonus: "10 credits" }` @guard(`.user.credits > 50`)
Check array lengths:
GET /process/items
|> jq: `{ items: [1, 2, 3, 4, 5] }`
|> jq: `{ processed: .items | map(. * 2) }` @guard(`(.items | length) >= 3`)
Check nested properties:
GET /api/user/preferences
|> jq: `{
user: {
settings: {
notifications: {
email: true,
push: false
}
}
}
}`
|> jq: `. + { sendEmail: true }` @guard(`.user.settings.notifications.email == true`)
|> jq: `. + { sendPush: true }` @guard(`.user.settings.notifications.push == true`)
Handle optional fields:
GET /api/optional/feature
|> jq: `{ feature: null, fallback: "default" }`
|> jq: `{ source: "feature" }` @guard(`.feature != null`)
|> jq: `{ source: "fallback" }` @guard(`.feature == null`)
Chain conditional steps:
pipeline conditionalProcessing =
|> jq: `{ stage: 1 }` @guard(`.enabled != false`)
|> jq: `. + { stage: 2 }` @guard(`.stage == 1`)
|> jq: `. + { stage: 3 }` @guard(`.stage == 2`)
|> jq: `. + { complete: true }`
Use guards as dispatch case conditions:
GET /dispatch/conditional
|> jq: `{ tier: "premium", verified: true }`
|> dispatch
case @guard(`.tier == "premium"`):
|> jq: `{ message: "Premium tier", features: ["all"] }`
case @guard(`.tier == "standard"`):
|> jq: `{ message: "Standard tier", features: ["basic"] }`
default:
|> jq: `{ message: "Free tier", features: [] }`
Conditional data transformation with validation, transformation, caching, and notification steps:
GET /pipeline/etl/:dataset
|> jq: `{
dataset: .params.dataset,
config: {
validate: true,
transform: true,
cache: true,
notify: false
},
rawData: [...]
}`
|> jq: `. + { validationErrors: (...) }` @guard(`.config.validate == true`)
|> jq: `. + { transformed: ... }` @guard(`.config.transform == true and (.validationErrors // 0) < 2`)
|> jq: `. + { cached: true }` @guard(`.config.cache == true`)
|> jq: `. + { notificationSent: true }` @guard(`.config.notify == true`)
|> jq: `{ dataset: .dataset, status: "complete" }`
| Feature | @guard | @flag / @env |
|---|---|---|
| Evaluation | Runtime data | Static configuration |
| Syntax | JQ expression | Tag name |
| Based on | Request state | Environment/flags |
| Flexibility | Any condition | Predefined flags |
| Use case | Data-driven logic | Feature toggles |
The @when tag enables conditional execution based on transient runtime conditions set dynamically via Lua's setWhen() function. Unlike @flag (persistent configuration), @when is designed for request-specific classification and routing.
@when(condition-name): Execute only if condition is true@!when(condition-name): Execute only if condition is false
Conditions are set in Lua:
setWhen("is_admin", true)
setWhen("is_premium", user.premium)Use Lua middleware to classify the request:
GET /test-when-basic
|> lua: `setWhen("is_admin", true); return request`
|> dispatch
case @when(is_admin):
|> jq: `{ role: "admin", access: "full" }`
default:
|> jq: `{ role: "guest", access: "limited" }`
Feature flags and dynamic conditions are separate:
GET /test-when-vs-flag-isolation
|> lua: `setWhen("beta", true); return request`
|> dispatch
case @flag(beta):
|> jq: `{ error: "BUG: beta is a condition, not a flag" }`
case @when(beta):
|> jq: `{ success: true, message: "Correctly matched @when(beta)" }`
Check for absence of condition:
GET /test-when-negated
|> lua: `setWhen("is_premium", false); return request`
|> dispatch
case @!when(is_premium):
|> jq: `{ message: "Free tier - upgrade for more features" }`
case @when(is_premium):
|> jq: `{ message: "Premium tier - all features unlocked" }`
Multiple condition names in one @when tag require ALL to be true:
GET /test-when-multiple
|> lua: `
setWhen("is_authenticated", true)
setWhen("is_admin", true)
return request
`
|> dispatch
case @when(is_authenticated,is_admin):
|> jq: `{ access: "admin_panel", level: "superuser" }`
case @when(is_authenticated):
|> jq: `{ access: "dashboard", level: "user" }`
default:
|> jq: `{ access: "login", level: "anonymous" }`
Conditions are available via $context.conditions:
GET /test-when-context-jq
|> lua: `setWhen("from_mobile", true); setWhen("dark_mode", false); return request`
|> jq: `{
conditions: $context.conditions,
from_mobile: $context.conditions.from_mobile,
dark_mode: $context.conditions.dark_mode
}`
Use context.conditions or getWhen():
GET /test-when-context-lua
|> lua: `setWhen("test_condition", true); return request`
|> lua: `
return {
conditions = context.conditions,
test_condition = context.conditions.test_condition or false
}
`
Or with getWhen():
GET /test-get-when
|> lua: `setWhen("feature_x", true); return request`
|> lua: `
local hasFeatureX = getWhen("feature_x")
local hasFeatureY = getWhen("feature_y") -- not set, returns false
return {
feature_x = hasFeatureX,
feature_y = hasFeatureY
}
`
Use @when tags on any pipeline step, not just in dispatch:
GET /test-when-on-step
|> jq: `{ base: "data" }`
|> lua: `setWhen("add_metadata", true); return request`
|> jq: `. + { metadata: { timestamp: "2024-01-01" } }` @when(add_metadata)
|> jq: `. + { skipped: true }` @when(never_set_condition)
Combine with and / or:
GET /test-dispatch-and
|> lua: `
setWhen("authenticated", true)
setWhen("admin", true)
return request
`
|> dispatch
case @when(authenticated) and @when(admin):
|> jq: `{ access: "admin_panel" }`
default:
|> jq: `{ access: "denied" }`
Mix with negated conditions:
GET /test-dispatch-negated-expr
|> lua: `
setWhen("authenticated", true)
setWhen("banned", false)
return request
`
|> dispatch
case @when(authenticated) and @!when(banned):
|> jq: `{ status: "welcome" }`
default:
|> jq: `{ status: "blocked" }`
Request Classification:
-- Classify device type
local userAgent = request.headers["user-agent"] or ""
setWhen("mobile", string.find(userAgent:lower(), "mobile") ~= nil)
setWhen("tablet", string.find(userAgent:lower(), "tablet") ~= nil)Access Control:
-- Check user permissions
setWhen("is_authenticated", request.user ~= nil)
setWhen("is_admin", request.user and request.user.role == "admin")
setWhen("is_premium", request.user and request.user.premium == true)Feature Eligibility:
-- Determine feature access
local score = calculateUserScore(request.user)
setWhen("high_quality_user", score > 80)
setWhen("eligible_for_beta", score > 50 and request.user.verified)| Feature | @when | @flag | @guard |
|---|---|---|---|
| Set by | setWhen() in Lua |
Config/setFlag() |
N/A (inline JQ) |
| Lifetime | Request-scoped | Persistent | N/A (evaluated per-step) |
| Evaluation | Tag evaluation | Tag evaluation | Runtime JQ eval |
| Namespace | Conditions | Flags | N/A |
| Use case | Request classification | Feature toggles | Data-driven logic |
- Context & Metadata - Accessing
$context.conditionsand$context.flags - DSL Syntax - Complete tag reference and syntax guide
- Testing & BDD - Testing dispatch, foreach, guards, and conditions