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
}