Skip to main content

Actions

An Action is a named sequence of steps that process data. Actions are the building blocks of your pipeline logic.

Basic structure

action ActionName {
// Step 1
get "/endpoint"

// Step 2
for item in response {
// Nested steps
}

// Step 3
store response -> storeName { key: .id }
}

Step types

Actions can contain the following step types:

StepDescription
get, post, put, patch, deleteHTTP requests
callOAS operation call
for...in...whereIteration with optional filtering
map...->Data transformation
apply...toApply a named transform
validateConstraint checking
store...->Data persistence
matchPattern matching with flow control
letBind a variable
waitWait for a webhook or callback
pauseResource-free long pause

HTTP request steps

Fetch data from APIs:

action FetchData {
// Simple GET
get "/users"

// Query parameters go in the path string
get "/users?limit=100&offset=0"

// With pagination
get "/users" {
paginate: page(page, 100),
until: length(response.users) == 0
}

// POST with body
post "/users" {
body: {
name: "John",
email: "john@example.com"
}
}
}

Iteration steps

Process collections:

action ProcessUsers {
get "/users"

// Iterate all items
for user in response.users {
// Process each user
}

// With filtering
for user in response.users where .status == "active" {
// Process only active users
}
}

Transformation steps

Transform data shapes:

action TransformData {
get "/users"

for user in response.users {
map user -> StandardUser {
id: .id,
fullName: .firstName + " " + .lastName,
email: .email,
createdAt: .created_at
}

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

Each field's right-hand side is an expression. Built-in functions include length, sum, count, first, last, round, floor, ceil, now, and env; + concatenates strings. See the Vague documentation for the full expression syntax.

Validation steps

Check data constraints:

action ValidateData {
get "/users"

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

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

Each assume takes a single condition expression. A failed assumption throws a ValidationError and aborts the mission, so validation is a hard gate, not a warning.

Store steps

Persist data:

action SaveData {
get "/users"

// Store entire response
store response -> allData

// Store with key
store response.users -> users { key: .id }

// Upsert mode
store response.users -> users { key: .id, upsert: true }

// Partial update
store response.users -> users { key: .id, partial: true }
}

Pattern matching steps

Match arms route on schema name, with an optional where guard or a _ wildcard. There are no object, array, or literal patterns. The matched schemas are defined at the mission level:

action HandleResponse {
get "/users"

match response {
AuthError where .code == 401 -> jump RefreshToken then retry,
RateLimitError where .code == 429 -> retry { maxAttempts: 5 },
ErrorResponse -> abort "API error",
UsersResponse -> continue,
_ -> abort "Unexpected response"
}
}

Arm right-hand sides are a flow directive (continue, skip, abort, retry, queue, jump), a single step, or a { ... } block of steps. abort takes an optional string literal only.

Nested actions

Actions can reference other actions via jump:

action Main {
get "/data"

match response {
AuthError -> jump RefreshAuth then retry,
_ -> continue
}
}

action RefreshAuth {
post "/auth/refresh" { body: { token: env("REFRESH_TOKEN") } }
// Token is automatically used for subsequent requests
}

Action composition in pipelines

Actions are composed in the run statement:

mission DataPipeline {
action Fetch {
// ...
}
action Transform {
// ...
}
action Export {
// ...
}

// Sequential
run Fetch then Transform then Export

// Parallel groups
run [FetchA, FetchB] then Merge then Export
}

Variable scope

Variables are scoped to their action and nested contexts:

action ProcessData {
get "/users" // response is set

for user in response.users {
// user is available here
// response is still available

map user -> processed {
// user and response available
}
// processed is available
}

// user is no longer available here
// response is still available
}

Best practices

Single responsibility

Each action should do one thing well:

// Good: focused actions
action FetchUsers {
get "/users"
store response -> rawUsers
}

action TransformUsers {
for user in rawUsers {
map user -> StandardUser {
// field mappings omitted
}
store user -> users { key: .id }
}
}

// Avoid: doing too much
action DoEverything {
get "/users"
get "/orders"
// transform both
// export to multiple places
}

Handle errors at action boundaries

action FetchWithErrorHandling {
get "/users"

match response {
ErrorResponse -> queue failures,
_ -> store response -> users { key: .id }
}
}

Use descriptive names

// Good
action FetchActiveCustomersWithOrders { }
action TransformToQuickBooksFormat { }
action ExportToDataWarehouse { }

// Avoid
action Step1 { }
action Process { }