Working with data
Querying with TaxiQL
Introduction
TaxiQL is Taxi’s query language for fetching and transforming data across your systems. Rather than writing integration code against specific APIs or databases, TaxiQL lets you declare what data you want using semantic types.
When you write a TaxiQL query, you’re describing the meaning of the data you need, not where to find it. Query engines like Orbital use these semantic types to automatically discover and orchestrate the necessary service calls - whether that’s a simple database query, or a complex flow across REST APIs, message queues, and serverless functions.
This semantic approach means your queries remain stable even as your architecture evolves. As services change their APIs or data moves between systems, TaxiQL adapts automatically, eliminating the traditional maintenance burden of integration code.
Basic syntax
The basic syntax of a TaxiQL query looks like this:
// Find all the people
find { Person[] }
// Find a person named Jim
find { Person( FirstName == 'Jim' ) }
// Find all the people named Jim
find { Person[]( FirstName == 'Jim' ) }
// Find a stream of person events from somewhere
stream { PersonEvents }
Here’s some interactive examples:
Fetch a list of data from a service
Fetches a list of Person[]
instances.
Server-side filtering
Adding contraints to the data being fetched controls the APIs that are called, only calling operations that satisfy the constraints
Learn more about server-side filtering and constraints
Client side filtering
Fetches data by calling an operation, then filters the result
Learn more about client-side filtering and expressions
Navigating data structures
You can access properties in models in two ways:
- Using semantic types (preferred) - `Customer::AddressLine“
- Using property names (classic, but not recommended) - `customer.addressLine“
You can also mix ‘n’ match these approaches.
Use types to stay decoupled and prevent breaking changes
Using types to access data keeps systems loosely coupled.
When consumers request data by its type, values are returned regardless of field names or nesting structure.
As schemas evolve, type-based access remains resilient to structural changes.
Using types to traverse data
Use the ::
operator to perform deep traversal through data structures:
// Find AddressLine1 at any depth within Customer
Customer::AddressLine1
// Chain deep traversals
// Find Address anywhere beneath Customer, and then PostCode anywhere beneath Address
Customer::Address::PostCode
The :: operator performs deep traversal, searching recursively through all nested levels of the data structure, not just immediate fields.
eg:
// Finds an instance of AddressLine1 anywhere in a Customer model
Customer::AddressLine1
Ambiguous results return null
When writing an expression like Customer::AddressLine1
, the result must select exactly one field, otherwise the statement is ambiguous, and the query engine returns null
. See Uniqueness for more information.
Using types to traverse child properties
This example shows three different approaches to requesting a field by it's type, each with increasing specificity
Uniqueness and type traversal
When requesting data using a type, ambiguity arises if there’s more than one location of a type.
If the requested type is ambiguous within the data (ie., there are multiple instances found when a single instance was requested), then Taxi returns null
.
To resolve this ambiguity, you have three options:
- Request all instances using array syntax
- Use structural navigation with property names
- Use structural navigation with type access syntax
For example:
Multiple values return null
In this query, PersonName
is not unique, so returns null
Instead, you can either select all instances (eg: in the preceding example, by requesting PersonName[]
instead of PersonName
), or add selectors to make the
selected instance unambiguous:
Resolving type ambiguity
Either select the array, or use qualifiers to make the selected instance unambiguous
Using property names
You can reference data using property names, by using the dot-selector .
.
eg:
model Customer {
profile: Profile
}
model Profile {
email: EmailAddress
}
// Property access - requires exact structure
customer.profile.email
Accessing properties by name
Access properties by name using standard 'dot' syntax
Mixing approaches
Combine traversal with property access:
// Type traversal then property
Customer::Address.streetName
// Property access then traversal
customer.addresses::PostCode
Constraints and filtering
Server-side constraints
Constraints limit which services are called based on their declared capabilities:
// Only calls services that can filter by FirstName
find { Customer(FirstName == 'Jimmy') }
// Multiple constraints
find { Customer[](
FirstName == 'Jimmy' &&
LastName == 'Smith'
) }
Services declare their constraint support:
service CustomerService {
// Declares support for FirstName and LastName filtering
operation findCustomersWithName(
@PathVariable first : FirstName,
@PathVariable last : LastName
): Customer(FirstName == first && LastName == last)
}
Client-side filtering
For additional filtering after data retrieval:
// Filter applied after data is fetched
find { Customer[].filter(FirstName -> FirstName == 'Jimmy') }
// Multiple conditions
find {
Customer[].filter(customer ->
customer::FirstName == 'Jimmy' &&
customer::Age > 21
)
}
Performance consideration
Server-side constraints are more efficient as they reduce data transfer. Client-side filtering is more flexible but may result in more data being transferred.
Given statements
Given statements make data available to your query without constraining which operations are called:
// Basic given statement
given { EmailAddress = 'jimmy@demo.com' }
find { Customer }
// With variable name
given { email : EmailAddress = 'jimmy@demo.com' }
find { Customer }
// Multiple values
given {
status : OrderStatus = 'PENDING'
customerId : CustomerId = '123'
}
find { Order[] }
Given vs constraints
Understanding the difference is crucial:
// Given: makes data available but doesn't restrict operations
given { status : OrderStatus = 'PENDING' }
find { Order[] } // May return orders of any status
// Constraint: restricts which operations can be called
given { status : OrderStatus = 'PENDING' }
find { Order[](OrderStatus == status) } // Only returns pending orders
Given doesn't constrain data
Providing data in given
makes data available that can be used with API calls -- but it doesn't limit which operations can be called. To filter results or specify exact data requirements, use constraints in your find
statement.
Basic projections
Projections transform and enrich data:
// Project to a different structure
find { Movie[] } as {
title : MovieTitle
director : DirectorName
rating : RottenTomatoesScore
}[]
// Project to a named type
find { Book[] } as BookAndAuthor[]
// Select specific fields
find { Order[] } as {
id // Field shorthand
status
total
}[]
Named queries
Save and reuse queries:
// Simple named query
query PendingOrders {
find { Order[](Status == 'PENDING') }
}
// Parameterized query
query FindOrdersByStatus(status: OrderStatus) {
find { Order[](Status == status) }
}
Including and excluding services
Control which services are called:
// Only use specific services
find { Film[] }
using {
FilmService::getFilms, // Specific operation
ReviewService // Entire service
}
// Exclude specific services
find { Film[] }
excluding {
ImdbApi, // Exclude entire service
RottenTomatoes::getReviews // Exclude specific operation
}
Supported operators
Symbol | Meaning |
---|---|
== | Equal to |
!= | Not equal to |
> | Greater than |
>= | Greater than or equal to |
< | Less than |
<= | Less than or equal to |
Next steps
- Learn about projections for transforming data
- Explore expressions and type traversal for navigating data
- Understand functions for data manipulation