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

For detailed expression syntax, see the Vague documentation.

String operations

map user -> Output {
fullName: concat(.firstName, " ", .lastName),
email: lowercase(.email),
initials: concat(substring(.firstName, 0, 1), substring(.lastName, 0, 1)),
domain: split(.email, "@")[1]
}

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

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

Pattern matching in maps

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
}
}

Mapping arrays

map order -> Output {
id: .id,
items: .lineItems.map(item => {
productId: item.product_id,
name: item.product_name,
quantity: item.qty,
price: item.unit_price
})
}

Combining data

From multiple sources

action EnrichOrders {
for order in orders {
get concat("/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 }
}
}

Merging objects

map source -> Output {
...baseData,
...additionalData,
overriddenField: "new value"
}

Date transformations

map event -> Output {
timestamp: parseDate(.created_at),
formattedDate: formatDate(.created_at, "YYYY-MM-DD"),
year: year(.created_at),
dayOfWeek: dayOfWeek(.created_at)
}

Null handling

Default values

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

Null checks

map user -> Output {
hasEmail: .email != null,
displayEmail: if .email != null then .email else "Not provided"
}

Type coercion

map data -> Output {
id: toString(.id),
count: toNumber(.count),
isActive: toBoolean(.active),
tags: toArray(.tags)
}

Computed fields

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

Renaming fields

// Transform API response to standard format
map xeroInvoice -> StandardInvoice {
id: .InvoiceID,
number: .InvoiceNumber,
customerId: .Contact.ContactID,
customerName: .Contact.Name,
amount: .Total,
status: lowercase(.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
}

Grouping and aggregation

map orders -> Summary {
totalOrders: length(orders),
totalRevenue: sum(orders.map(.total)),
averageOrder: sum(orders.map(.total)) / length(orders),
byStatus: {
pending: length(filter(orders, .status == "pending")),
completed: length(filter(orders, .status == "completed"))
}
}

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
items: .LineItems.map(item => {
description: item.Description,
quantity: item.Quantity,
unitPrice: item.UnitAmount,
total: item.LineAmount,
taxAmount: item.TaxAmount or 0
}),

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

// Status
status: lowercase(.Status),
isPaid: .Status == "PAID",

// Dates
date: parseDate(.DateString),
dueDate: parseDate(.DueDateString),

// Metadata
createdAt: parseDate(.UpdatedDateUTC),
source: "xero"
}

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

run TransformInvoices
}