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.

Schema
Play with this snippet by editing it here, or edit it on Taxi Playground
Result
Query failed

Server-side filtering

Adding contraints to the data being fetched controls the APIs that are called, only calling operations that satisfy the constraints

Schema
Play with this snippet by editing it here, or edit it on Taxi Playground
Result
Query failed

Learn more about server-side filtering and constraints


Client side filtering

Fetches data by calling an operation, then filters the result

Schema
Play with this snippet by editing it here, or edit it on Taxi Playground
Result
Query failed

Learn more about client-side filtering and expressions

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

Schema
Play with this snippet by editing it here, or edit it on Taxi Playground
Result
Query failed

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:

  1. Request all instances using array syntax
  2. Use structural navigation with property names
  3. Use structural navigation with type access syntax

For example:

Multiple values return null

In this query, PersonName is not unique, so returns null

Schema
Play with this snippet by editing it here, or edit it on Taxi Playground
Result
Query failed

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

Schema
Play with this snippet by editing it here, or edit it on Taxi Playground
Result
Query failed

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

Schema
Play with this snippet by editing it here, or edit it on Taxi Playground
Result
Query failed

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

SymbolMeaning
==Equal to
!=Not equal to
>Greater than
>=Greater than or equal to
<Less than
<=Less than or equal to

Next steps

Previous
Services
Next
Transforming data (projections)