Skip to main content

Validation

Validate steps check data constraints before processing or storing. They help ensure data quality and catch issues early.

A validate step needs a target expression. Each assume takes a single condition. If any condition is false, the step throws a ValidationError and the mission aborts; there are no warning-level assumptions and no custom message syntax.

Basic syntax

validate target {
assume constraint1,
assume constraint2
}

To validate the target against a named Vague schema before checking inline assumptions, add as SchemaName:

validate response as Invoice {
assume .status != "deleted"
}

Named validation checks required and optional fields, primitive and referenced types, collection elements, ranges, choices, schema constraints, assumptions, and invariants.

Simple validation

action ValidateUsers {
get "/users"

for user in response.users {
validate user {
assume .id is string,
assume length(.name) > 0,
assume .email is string
}

store user -> users { key: .id }
}
}

Constraint types

Type checking

validate data {
assume .id is string,
assume .count is number,
assume .active is boolean,
assume .tags is array,
assume .metadata is object,
assume .deletedAt is null
}

Existence checks

validate data {
assume .id != null,
assume .name != null,
assume .email != null
}

String constraints

There are no string-matching operators or functions like contains, startsWith, or endsWith. You can check presence, type, and length:

validate user {
assume .name is string,
assume length(.name) > 0,
assume length(.name) < 100,
assume .email != null
}

Numeric constraints

validate order {
assume .quantity > 0,
assume .price >= 0,
assume .discount >= 0 and .discount <= 100,
assume .total == .price * .quantity
}

Array constraints

validate response {
assume length(.items) > 0,
assume length(.items) <= 100
}

Comparison

validate event {
assume .endDate >= .startDate,
assume .createdAt <= now()
}

Complex constraints

Logical operators

validate user {
assume .status == "active" or .status == "pending",
assume .age >= 18 and .age <= 120,
assume not (.status == "banned")
}

Conditional validation

validate order {
// If discount is present, it must be valid
assume .discount == null or (.discount >= 0 and .discount <= 50),

// If status is shipped, must have tracking
assume .status != "shipped" or .trackingNumber != null
}

Validation behavior

A failed assume throws and aborts the mission. There is no warning level, so validation is always strict:

validate user {
assume length(.name) > 0 // Aborts the mission if this is false
}
// This line is only reached when every assumption holds

If you'd rather route invalid records instead of aborting, skip validate and use a match step with a guard:

schema User {
id: string,
email: string
}

action RouteUsers {
get "/users"

for user in response.users {
match user {
User -> store user -> users { key: .id },
_ -> skip
}
}
}

Validating nested data

validate order {
assume .id is string,
assume .customer.id is string,
assume .customer.email is string,
assume length(.items) > 0,
assume .items[0].quantity > 0
}

Validating arrays

action ValidateAllItems {
get "/orders"

for order in response.orders {
// Validate order-level
validate order {
assume .id is string,
assume .total > 0
}

// Validate each item
for item in order.items {
validate item {
assume .productId is string,
assume .quantity > 0,
assume .price >= 0
}
}
}
}

Routing invalid records

When you don't want a failure to abort the mission, route records with match and a guarded wildcard instead of validate:

action RouteInvalid {
get "/users"

for user in response.users {
match user {
_ where user.email == null -> {
store { userId: user.id, error: "Missing email" } -> validationErrors { key: user.id }
skip
},
_ where user.age < 18 -> {
store { userId: user.id, error: "User under 18" } -> validationErrors { key: user.id }
skip
},
_ -> store user -> validUsers { key: .id }
}
}
}

Validation before store

Validate before storing, so bad records abort early:

action SafeStore {
get "/data"

for item in response.data {
validate item {
assume .id is string,
assume .value is number
}

store item -> data { key: .id }
}
}

Validation schemas

Use schemas for reusable validation:

schema ValidUser {
id: string,
name: string,
email: string
}

action ValidateAgainstSchema {
get "/users"

for user in response.users {
match user {
ValidUser -> store user -> users { key: .id },
_ -> store user -> invalidUsers { key: .id }
}
}
}

Built-in functions in constraints

The expression language has a small set of built-in functions. The ones useful in constraints are length, sum, count, first, last, round, floor, ceil, and now:

validate data {
// Length and count
assume length(.name) > 0,
assume length(.items) > 0,

// Numeric
assume round(.price) == .price,
assume sum(.amounts) > 0,

// Timestamps (compared as strings or dates)
assume .createdAt != null
}

Complete example

mission DataValidation {
source API { auth: bearer, base: "https://api.example.com" }

store validOrders: file("valid-orders")
store invalidOrders: file("invalid-orders")
store validationErrors: file("validation-errors")

schema ValidOrder {
id: string,
customerId: string,
items: array,
total: number
}

action ValidateOrders {
get "/orders"

for order in response.orders {
// Type validation
validate order {
assume .id is string,
assume .customerId is string,
assume .items is array,
assume .total is number
}

// Business rule validation
validate order {
assume length(.items) > 0,
assume .total > 0,
assume .status == "pending" or .status == "confirmed"
}

// Route based on schema match
match order {
ValidOrder where order.total > 0 and length(order.items) > 0 -> {
store order -> validOrders { key: .id }
},
_ -> {
store {
orderId: order.id,
order: order,
reason: "Failed validation"
} -> invalidOrders { key: order.id }
}
}
}
}

run ValidateOrders
}

Best practices

Validate early

action Process {
get "/data"

// Validate immediately after fetch
validate response {
assume .data is array,
assume length(.data) > 0
}

// Then process
for item in response.data { }
}

Use specific constraints

// Good: specific constraints
validate user {
assume .email is string,
assume length(.email) > 5,
assume .age >= 18
}

// Avoid: too loose
validate user {
assume .email != null
}

Route failures instead of aborting

When you want to record bad records rather than stop, route them with a guarded match:

action RouteWithLogging {
for item in items {
match item {
_ where item.id == null -> {
store { itemId: "unknown", field: "id", error: "Missing" } -> errors { key: "unknown" }
skip
},
_ -> continue
}
}
}