Skip to content

Array Mapping

When you need to return a list of items in GraphQL, you cannot write standard for or map loops. Instead, The Bridge provides a declarative syntax for mapping array elements into your expected output shape, complete with explicit control flow to filter or halt the iteration.

To map an array, you use the [] as <variableName> operator followed by a scoping block.

bridge Query.getJourneys {
with routerApi as router
with output as o
# Iterate over every element in router.journeys
o.journeys <- router.journeys[] as j {
# Map fields for each individual element
.label <- j.label
.departureTime <- j.departure
}
}

When the engine executes an array mapping block, it creates a Shadow Scope (or shadow tree) for every single element in the array.

The variable j represents the current element being processed. Because each element executes in its own isolated scope, you can nest array mappings arbitrarily deep without worrying about variable collisions.

Array mappings can introduce tool handles inside the loop body with with. Those handles are writable only inside the current loop scope.

o.items <- api.items[] as item {
with std.httpCall as fetchItem
fetchItem.value <- item.id
.detail <- fetchItem.data
}

If repeated elements can resolve to the same tool input, add memoize to the loop-scoped handle:

o.items <- api.items[] as item {
with std.httpCall as fetchItem memoize
fetchItem.value <- item.id
.detail <- fetchItem.data
}

Memoization is scoped to that declared handle. A nested loop can declare its own memoized handle without sharing the parent cache.

You can also use aliases inside loops purely for readability, without triggering any tools. If your iterator has deeply nested data, bind it to a short variable:

o.list <- api.items[] as it {
# Bind a deep sub-object to a cleaner name
alias it.metadata.authorInfo as author
.name <- author.name
.role <- author.role
}

APIs often return messy arrays containing nulls, missing IDs, or corrupt data. Instead of returning null to the frontend, you can use the explicit control flow keywords continue and break on the right side of any fallback gate (??, ||, catch) to filter the array directly.

The continue keyword instructs the engine to omit this specific item from the final array, but keep looping and processing the rest of the elements.

o.items <- billingApi.items[] as item {
# If the item is missing an ID, skip it entirely.
# The frontend will not receive a null object; the item just won't exist.
.id <- item.id ?? continue
.name <- item.name
}

The break keyword instructs the engine to stop processing the array entirely and return the items processed up to that point.

o.items <- searchApi.results[] as item {
.id <- item.id
# If we hit an item without a price, halt the entire array map.
# Returns only the valid items that came before it.
.price <- item.price ?? break
}

Multi-Level Control Flow (break N, continue N)

Section titled “Multi-Level Control Flow (break N, continue N)”

When working with deeply nested arrays (e.g., mapping categories that contain lists of products), you may want an error deep inside the inner array to skip the outer array element.

You can append a number to break or continue to specify how many loop levels the signal should pierce.

o.catalogs <- api.catalogs[] as cat {
.id <- cat.id
.products <- cat.products[] as prod {
.name <- prod.name
# If a product has a fatal data corruption, skip the ENTIRE catalog.
# 'continue 1' would just skip this product.
# 'continue 2' skips this product AND the catalog it belongs to!
.sku <- prod.sku ?? continue 2
}
}

Use Case 1: Filtering an Array Before Mapping

Section titled “Use Case 1: Filtering an Array Before Mapping”

While continue is great for dropping items based on missing structural fields, sometimes you want to pre-filter a massive array based on explicit logic before mapping it. You can do this using the built-in std.filterObject tool.

bridge Query.getActiveAdmins {
with std.filterObject as filter
with usersApi as api
with output as o
# 1. Provide the array to filter
filter.in <- api.users
# 2. Define the exact criteria to match
filter.role = "admin"
filter.isActive = true
# 3. Map over ONLY the filtered results!
o.admins <- filter[] as admin {
.id <- admin.id
.name <- admin.name
}
}

If you have an array of IDs and you need to fetch detailed information from an external API for every single item, you can perform a “Fanout.”

By combining array mapping with a Pipe and an alias, the engine will dynamically fork the tool and fire a parallel API request for every element in the array.

tool getUserDetail from std.httpCall {
.baseUrl = "https://api.example.com/users"
.method = "GET"
# We leave .path blank so we can inject it dynamically!
}
bridge Query.getEnrichedUsers {
with getUserDetail
with input as i
with output as o
o.enrichedUsers <- i.userIds[] as id {
# 1. Dynamically build the path for this specific element
# 2. Pipe the path into the HTTP tool
# 3. Safely swallow any 500 errors so one bad user doesn't break the list
# 4. Cache the result for this element as 'detail'
alias getUserDetail?.path:"/{id}" catch null as detail
# Map the resulting HTTP response back to the array
# If 'detail' is null (from the catch), we skip this user!
.id <- id
.email <- detail.email ?? continue
.status <- detail.status
}
}

If duplicate inputs are common, prefer a loop-scoped with ... memoize handle over a pipe alias so repeated requests can reuse prior results within that handle’s cache.