Skip to main content

Map transformations

Map steps transform data from one shape to another. They're used to normalize API responses, enrich data, and prepare data for storage.

Basic syntax

map sourceData -> TargetSchema {
field: expression,
anotherField: expression
}

Simple mapping

action TransformUser {
get "/users/123"

map response -> User {
id: .id,
name: .name,
email: .email
}

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

Field access

Direct access

map user -> Output {
id: .id,
name: .name
}

Nested access

map user -> Output {
userId: .id,
street: .address.street,
city: .address.city,
country: .address.country
}

Array access

map order -> Output {
firstItem: .items[0],
lastItem: .items[length(.items) - 1]
}

Expressions

A field's value is a normal expression. For detailed expression syntax, see the Expressions page and the Vague documentation.

String operations

The + operator concatenates strings. There are no string functions like lowercase, substring, or split:

map user -> Output {
fullName: .firstName + " " + .lastName,
greeting: "Hello, " + .name
}

Numeric operations

map order -> Output {
subtotal: .price * .quantity,
tax: .price * .quantity * 0.1,
total: .price * .quantity * 1.1,
discounted: .total * (1 - .discountPercent / 100)
}

Conditional expressions

Use the ternary operator condition ? a : b. There's no if/then/else expression form:

map user -> Output {
status: .active ? "Active" : "Inactive",
tier: .totalSpent > 10000 ? "Gold"
: .totalSpent > 5000 ? "Silver"
: "Bronze"
}

Pattern matching in maps

A match expression compares a value against literal patterns with => and returns the matching arm's value:

map order -> Output {
statusLabel: match .status {
"pending" => "Awaiting Processing",
"processing" => "In Progress",
"shipped" => "On the Way",
"delivered" => "Completed",
_ => "Unknown"
}
}

Nested mapping

Static nested objects

map user -> Output {
id: .id,
profile: {
name: .name,
email: .email,
phone: .phone
},
metadata: {
createdAt: .created_at,
updatedAt: .updated_at
}
}

Combining data

From multiple sources

action EnrichOrders {
for order in orders {
get "/customers/{order.customerId}"

map order -> EnrichedOrder {
id: order.id,
total: order.total,
customer: {
id: response.id,
name: response.name,
email: response.email
}
}

store order -> enrichedOrders { key: .id }
}
}

Null handling

Default values

The or operator returns its left side when truthy, otherwise the right side, which makes it a handy default:

map user -> Output {
name: .name or "Unknown",
email: .email or "no-email@example.com",
phone: .phone or null
}

Null checks

map user -> Output {
hasEmail: not (.email == null),
displayEmail: not (.email == null) ? .email : "Not provided"
}

Computed fields

length and sum are built in. sum takes an array of numbers:

map invoice -> Output {
id: .id,
lineItems: .items,
subtotal: sum(.amounts),
taxRate: 0.1,
tax: sum(.amounts) * 0.1,
total: sum(.amounts) * 1.1,
itemCount: length(.items)
}

Renaming fields

// Transform an API response to a standard format
map xeroInvoice -> StandardInvoice {
id: .InvoiceID,
number: .InvoiceNumber,
customerId: .Contact.ContactID,
customerName: .Contact.Name,
amount: .Total,
status: .Status,
createdAt: .DateString
}

Flattening nested data

map order -> FlatOrder {
orderId: .id,
orderDate: .createdAt,
customerName: .customer.name,
customerEmail: .customer.email,
shippingStreet: .shipping.address.street,
shippingCity: .shipping.address.city,
total: .total
}

Aggregation

length and sum work over arrays. There's no map, filter, or avg function, so aggregate over arrays you already have rather than deriving them inline:

map order -> Summary {
itemCount: length(.items),
totalRevenue: sum(.amounts),
averageItem: sum(.amounts) / length(.items)
}

Complete example

mission TransformXeroData {
source Xero { auth: oauth2, base: "https://api.xero.com/api.xro/2.0" }

store invoices: file("invoices")

action TransformInvoices {
get "/Invoices"

for invoice in response.Invoices {
map invoice -> StandardInvoice {
// Identifiers
id: .InvoiceID,
number: .InvoiceNumber,
type: match .Type {
"ACCREC" => "receivable",
"ACCPAY" => "payable",
_ => "unknown"
},

// Customer info
customer: {
id: .Contact.ContactID,
name: .Contact.Name,
email: .Contact.EmailAddress or null
},

// Line items (kept as-is from the source)
items: .LineItems,

// Totals
subtotal: .SubTotal,
tax: .TotalTax,
total: .Total,

// Status
status: .Status,
isPaid: .Status == "PAID",

// Dates
date: .DateString,
dueDate: .DueDateString,

// Metadata
createdAt: .UpdatedDateUTC,
source: "xero"
}

store invoice -> invoices { key: .id, upsert: true }
}
}

run TransformInvoices
}