Zion.app Headless BaaS Skill
Overview
This skill outlines how to build frontend applications utilizing Zion.app as a headless Backend-as-a-Service (BaaS). Zion exposes all backend interactions (database, actionflows, third-party APIs, and AI agents) through a single, unified GraphQL API.
- HTTP URL:
https://zion-app.functorz.com/zero/{projectExId}/api/graphql-v2
- WebSocket URL:
wss://zion-app.functorz.com/zero/{projectExId}/api/graphql-subscription
Token Acquisition & Authentication (CRITICAL)
To interact with authenticated endpoints, you must obtain a JWT token by logging in or registering. Unauthenticated requests are assigned an anonymous user role. The JWT can be obtained in two ways. One is via username + password login. The other one is by querying the Meta API & fetching runtime backend token. The token return in FETCH_DATA_VISUALIZER is the runtime backend token.
1. Username Registration & Login
You should ask user for username and password.
mutation AuthenticateWithUsername($username: String!, $password: String!, $register: Boolean!) {
authenticateWithUsername(username: $username, password: $password, register: $register) {
account { id, permissionRoles }
jwt { token }
}
}
Note: Both mutations return FZ_Account which is a subset of the full account type. It contains only email, id, permissionRoles, phoneNumber, profileImageUrl, roles, and username.
Developer Authentication with Zion.app (Meta API)
If you need to interact directly with the Zion platform (Meta API) to fetch project schemas, list projects, or authenticate as an admin to the runtime backend, follow these steps:
1. Acquire Developer JWT Token
There are two ways to acquire a developer token: via OAuth Flow or via Email/Password login.
Option A: OAuth Flow
Set up a local HTTP server to receive the OAuth callback. Open the Zion authentication endpoint (https://auth.functorz.com/login) in the browser and wait for the token parameter.
You can run the bundled authentication script:
cd ~/.agents/skills/zion-baas-skill/scripts
npm run auth
Option B: Email/Password Login
You can directly login with an email and password using the Meta API:
cd ~/.agents/skills/zion-baas-skill/scripts
npm run auth:email <email> <password>
2. Querying the Meta API & Fetching Runtime Backend Token
Use the developer JWT token as a Bearer token against the Meta API (https://zionbackend.functorz.com/api/graphql) to get the schema or data visualizer tokens. The data visualizer token grants administrative access to the project's runtime backend (zeroUrl).
You can run the bundled script to fetch the token. It requires the developer token to be present in .zion/credentials.yaml.
cd ~/.agents/skills/zion-baas-skill/scripts
npm run fetch-token -- <projectExId>
You can also search for projects or fetch their schema via the Meta API using the bundled meta script:
cd ~/.agents/skills/zion-baas-skill/scripts
# Search projects (returns project names and exIds)
npm run meta -- search-projects "optional search term"
# Fetch project schema (returns data models, actionflows, apis)
npm run meta -- fetch-schema <projectExId>
Credential Management & State Persistence
All credentials and project state MUST be persisted in the .zion directory at the root of the user's project in YAML format, typically in a file named .zion/credentials.yaml.
Required YAML Format
The credentials file must adhere to the following structure:
# .zion/credentials.yaml
developer_token:
token: "<your_developer_jwt_token>" # Used to communicate with zionbackend.functorz.com
expiry: "<timestamp_or_date_of_expiry>"
project:
exId: "<project_ex_id>"
name: "<project_name>"
admin_token:
token: "<runtime_backend_admin_token>"
expiry: "<timestamp_or_date_of_expiry>"
other_users:
- user_id: "<user_id>"
user_tags:
- "<notable_information_about_the_user_1>"
token:
token: "<user_jwt_token>"
expiry: "<timestamp_or_date_of_expiry>"
developer_token: The JWT token acquired via the OAuth flow, used against the Meta API (zionbackend.functorz.com).
project: Contains the core project identifiers (exId, name), the admin_token (data visualizer token) for the runtime backend, and other_users.
other_users: A list of authenticated users (e.g., test accounts) containing their user_id, helpful user_tags to identify their purpose, and their token.
expiry: All stored tokens must include an associated expiry.
Executing GraphQL Queries & Subscriptions via CLI
You can use the bundled scripts to quickly test GraphQL queries, mutations, and subscriptions from the command line without writing frontend boilerplate. These scripts automatically read the correct token from your project's .zion/credentials.yaml.
1. Execute a Query or Mutation
Pass your GraphQL query or mutation as a string.
cd ~/.agents/skills/zion-baas-skill/scripts
npm run gql -- <projectExId> <role> '<query_string>' '<optional_variables_json>'
<role>: Can be admin (uses data visualizer token), anonymous (no token), or a specific user_id (fetches token from other_users in .zion/credentials.yaml).
Example:
npm run gql -- myProjectEx123 admin 'query GetProject($id: Int!) { project(id: $id) { name } }' '{"id": 1}'
2. Listen to a Subscription
Pass your GraphQL subscription as a string.
cd ~/.agents/skills/zion-baas-skill/scripts
npm run subscribe -- <projectExId> <role> '<query_string>' '<optional_variables_json>'
The script will establish a WebSocket connection and continuously print events as they arrive until you kill it (Ctrl+C).
Database & GraphQL Schema Rules
- Use
zion MCP server or webfetch/curl to introspect the schema before making assumptions.
The GraphQL schema is automatically generated directly from the PostgreSQL data model.
CRITICAL CATCH-ALL RULE:
If you are ever in doubt about the exact structure, available fields, Enum values (like [Enum:ROUNDING_MODE]), or specific arguments for an endpoint, you must use the webfetch/curl tool to introspect the live GraphQL schema via the provided endpoint URL before making assumptions.
1. Naming Conventions & Root Operations
Each table generates corresponding root query, mutation, and subscription fields. For a table named [table]:
Queries
[table]: Fetch lists (supports where, order_by, distinct_on, limit, offset).
[table]_by_pk: Fetch single record by primary key (id).
[table]_aggregate: Aggregate queries (count, sum, avg, min, max).
[table]_group_by: Group records based on specified fields.
fz_[table]_by_[column]: Auto-generated spatial proximity search if the table contains a geo_point column.
- Relay API: If configured, generates a cursor-based pagination query returning a
Connection_[table] object.
Mutations
insert_[table], insert_[table]_one: Create records. Supports nested inserts and on_conflict (requires constraint and update_columns).
update_[table], update_[table]_by_pk: Modify records. Uses _set (replace) and _inc (increment numeric). where is required (!) for bulk updates. (Note: Hasura JSONB update operators like _append/_delete_key are not supported).
delete_[table], delete_[table]_by_pk: Remove records. where is required (!) for bulk deletes.
export_[table]: Trigger a data export task for the table.
Subscriptions
[table], [table]_by_pk, [table]_aggregate: Live queries mirroring their standard query counterparts.
2. Column Types and Data Mappings
Primitive Types
text → String, integer → Int, bigint, bigserial → bigint, float8 → Float8, decimal → Decimal, boolean → Boolean, jsonb → jsonb.
- Time/Date:
timestamptz, timetz, date, interval.
- Geo:
geo_point → geography.
Composite (Media) Types
Media columns (image, file, video) are structurally 1:N relations but act as fields.
- Single Media: Stored as
[column]_id (e.g., cover_image_id) referencing system asset tables (FZ_Image, FZ_File, FZ_Video).
- Media Lists: Types like
image_list, video_list, file_list map to GraphQL Arrays and are stored as [column]_ids (e.g., gallery_images_ids).
System-Managed Columns
id, created_at, updated_at are read-only and automatically managed. They cannot be used in mutation inputs (_set, _inc, insert inputs).
3. Relationships
Relationships are defined by foreign keys and determine GraphQL nested fields:
- 1:1 (One-to-One): Yields a single nested object (e.g.,
meta: post_meta).
- 1:N (One-to-Many): Yields an array (e.g.,
post_tags: [post_tag]) and an aggregate object (e.g., post_tags_aggregate).
4. Filtering (where clauses)
Filters rely on [table]_bool_exp.
Logical Operators
_and: [bool_exp], _or: [bool_exp], _not: bool_exp
Relation Filters
Navigate relationships using the relationship field name directly. The value is a nested [related_table]_bool_exp object.
- To-One Relationships (1:1, N:1): Filters the parent record based on the single related record's fields.
(e.g., Find posts where the author's name is "John":
author: { name: { _eq: ... } })
- To-Many Relationships (1:N, N:M): Uses
EXISTS semantics. The parent record is returned if any related record in the array matches the nested condition.
(e.g., Find posts that have at least one tag named "Tech": post_tags: { tag: { name: { _eq: ... } } })
Comparison Predicates (Strict Pattern)
Zion uses a strict Operator-First Pattern. A predicate must start with the operator. If it's a generic operator, the operand wrapper type must match the final evaluated type, not necessarily the column type.
Structure:
{
"_operator": {
"operand_type": {
"left_operand": { ... },
"right_operand": { ... }
}
}
}
- Operators:
- Comparison (Generic):
_eq, _neq, _gt, _lt, _gte, _lte
- Array (Generic):
_in, _nin
- Nullity (Generic, Unary):
_is_null, _is_not_null
- Text (String Pattern):
_like, _nlike, _ilike, _nilike, _similar, _nsimilar
- JSONB:
_contains, _contained_in, _has_key, _has_keys_any, _has_keys_all
- Boolean:
_is_true, _is_false
- Collection:
_is_empty, _is_not_empty
- Operand Definitions (
left_operand / right_operand):
- Literal:
{"literal": value}
- Column:
{"column": "field_name"}
- Function:
{"function_name": { ...args }}
- Operand Types (Determined by Final Value):
bigint_operand, text_operand, boolean_operand, timestamptz_operand, etc.
Example Predicate (Extract Month from Timestamp and check if = 12):
{
"_eq": {
"bigint_operand": {
"left_operand": {
"extract_timestamptz": { "time": { "column": "created_at" }, "unit": "MONTH" }
},
"right_operand": { "literal": "12" }
}
}
}
5. Aggregations, Window Functions, and Order By
Aggregation ([table]_aggregate)
Returns an aggregate query object containing two main fields:
nodes: An array of the actual table objects ([table!]!) containing the raw data rows that match the query's where, limit, offset, and order_by criteria.
aggregate: An object containing statistical calculations over the matched rows:
count(columns: [Enum], distinct: Boolean):
- If no columns are provided, it counts all rows (
COUNT(*)).
- If
distinct: true and 1+ columns are provided, it counts unique values or unique combinations of the specified columns.
- If
distinct: false and multiple columns are provided, the system restricts the count to only evaluate the first column in the array.
sum, avg: Only available for numeric columns.
max, min: Available for comparable columns (numeric, time, text).
Window Functions
Advanced analytical operations like ROW_NUMBER, RANK, DENSE_RANK, NTH_VALUE are supported in specific formula and window frame inputs.
Sorting (order_by)
Supports sorting by:
- Direct columns:
{ title: asc }
- Related 1:1 records:
{ author: { name: desc } }
- Aggregates of N:1 records:
{ post_tags_aggregate: { count: desc } }
- Vector Search: Text columns may support similarity sorting if the
TEXT_COLUMN_VECTOR_SORT extension is applied.
6. Formula Functions (Operands)
Functions are used inside operand wrappers by wrapping the uppercase function name around its arguments (e.g., {"EXTRACT_TIMESTAMPTZ": { "time": ..., "unit": ... }}).
Input Constraints & Semantics
- [TYPE]: Indicates a required column or scalar operand of that exact type.
- [TYPE?]: Indicates an optional operand (usually defaults to
0 or null).
- [ANY]: Accepts any column or scalar operand of any type.
- [NUMERIC]: Accepts
BIGINT, INTEGER, DECIMAL, FLOAT8, or BIGSERIAL.
- [COMPARABLE]: Accepts
NUMERIC types, TEXT, DATE, TIMESTAMPTZ, TIMETZ, or INTERVAL.
- [ANY[]]: Accepts an array operand of any type.
- [Enum:NAME]: Requires an explicit Enum value matching the
[NAME] definition (e.g., [Enum:DATE_UNIT] requires YEAR, MONTH, etc.).
- Nested Functions: Arguments can often be the output of other functions, provided the output type matches the expected input type.
Manipulation
CONCAT(items: [TEXT[]])
SUBSTRING(source_text: [TEXT], start_index: [BIGINT], end_index: [BIGINT])
LEFT(source_text: [TEXT], length: [BIGINT])
RIGHT(source_text: [TEXT], length: [BIGINT])
LOWER(source_text: [TEXT])
UPPER(source_text: [TEXT])
TRIM(text: [TEXT])
TRIM_TRAILING_ZERO(source_text: [TEXT])
REPEAT(text: [TEXT], times: [BIGINT])
ENCODE_URL(text: [TEXT])
DECODE_URL(text: [TEXT])
ARRAY_CONCAT(first_array: [ANY[]], second_array: [ANY[]])
SLICE(array: [ANY[]], start_index: [BIGINT], length: [BIGINT])
UNIQUE(array: [ANY[]])
COALESCE(array: [ANY[]])
Search & Replace
REPLACE_OCCURRENCES(source_text: [TEXT], search_text: [TEXT], replace_text: [TEXT], max_replacements: [BIGINT])
REPLACE_AT_POSITION(source_text: [TEXT], start_index: [BIGINT], length: [BIGINT], replace_text: [TEXT])
POSITION(source_text: [TEXT], search_text: [TEXT])
CONTAINS(source_text: [TEXT], search_text: [TEXT])
Regex
REGEX_EXTRACT(text: [TEXT], regex: [TEXT])
REGEX_REPLACE(text: [TEXT], regex: [TEXT], replacement: [TEXT])
REGEX_EXTRACT_ALL(text: [TEXT], regex: [TEXT])
REGEX_MATCH(text: [TEXT], regex: [TEXT])
Formatting & Utils
TEXT_DECIMAL_FORMAT(number: [DECIMAL], fraction_digits: [BIGINT], rounding_mode: [Enum:ROUNDING_MODE], clear_trailing_zeros: [BOOLEAN])
NUMBER_FORMAT(number: [DECIMAL], fraction_digits: [BIGINT], format: [Enum:NUMBER_FORMAT])
STRING_LEN(source_text: [TEXT])
RANDOM_TEXT(min_length: [BIGINT], max_length: [BIGINT], include_numbers: [BOOLEAN], include_lower_case: [BOOLEAN], include_upper_case: [BOOLEAN])
UUID()
JOIN(array: [TEXT[]], separator: [TEXT])
SPLIT(source_text: [TEXT], delimiter: [TEXT])
ARRAY_LEN(array: [ANY[]])
Arithmetic
ADD(value0: [NUMERIC], value1: [NUMERIC])
SUBTRACT(minuend: [NUMERIC], subtrahend: [NUMERIC])
MULTIPLY(value0: [NUMERIC], value1: [NUMERIC])
DIVIDE(dividend: [DECIMAL], divisor: [DECIMAL])
MODULO(dividend: [NUMERIC], divisor: [NUMERIC])
ABS(number: [NUMERIC])
POW(base: [NUMERIC], exponent: [NUMERIC])
LOG(base: [DECIMAL], argument: [DECIMAL])
Rounding & Formats
ROUND_UP(number: [DECIMAL])
ROUND_DOWN(number: [DECIMAL])
DECIMAL_FORMAT(number: [DECIMAL], fraction_digits: [BIGINT], rounding_mode: [Enum:ROUNDING_MODE])
Generators
RANDOM_BIGINT(min_length: [BIGINT], max_length: [BIGINT])
SEQUENCE(start: [BIGINT], end: [BIGINT], step: [BIGINT])
Current Time
CURRENT_DATE()
CURRENT_TIMETZ()
CURRENT_TIMESTAMPTZ()
Constructors
MAKE_DATE(years: [BIGINT], months: [BIGINT], days: [BIGINT])
MAKE_TIMETZ(hours: [BIGINT?], minutes: [BIGINT?], seconds: [BIGINT?], milliseconds: [BIGINT?])
MAKE_TIMESTAMPTZ(years: [BIGINT], months: [BIGINT], days: [BIGINT], hours: [BIGINT?], minutes: [BIGINT?], seconds: [BIGINT?], milliseconds: [BIGINT?])
MAKE_INTERVAL(years: [BIGINT], months: [BIGINT], weeks: [BIGINT], days: [BIGINT], hours: [BIGINT], minutes: [BIGINT], seconds: [BIGINT], milliseconds: [BIGINT])
FROM_DATE_AND_TIMETZ(date: [DATE], timetz: [TIMETZ])
Extraction & Formatting
EXTRACT_DATE(time: [DATE], unit: [Enum:DATE_UNIT])
EXTRACT_TIMETZ(time: [TIMETZ], unit: [Enum:TIME_UNIT])
EXTRACT_TIMESTAMPTZ(time: [TIMESTAMPTZ], unit: [Enum:TIMESTAMP_UNIT])
DATE_FORMAT(time: [DATE], format: [TEXT], language: [Enum:LANGUAGE])
TIMETZ_FORMAT(time: [TIMETZ], format: [TEXT], language: [Enum:LANGUAGE])
TIMESTAMPTZ_FORMAT(time: [TIMESTAMPTZ], format: [TEXT], language: [Enum:LANGUAGE])
DURATION_FORMAT(duration: [DECIMAL], unit: [Enum:DURATION_UNIT], format: [TEXT])
RELATIVE_DATE(time: [DATE], language: [Enum:LANGUAGE], hide_suffix: [BOOLEAN])
RELATIVE_TIMESTAMPTZ(time: [TIMESTAMPTZ], language: [Enum:LANGUAGE], hide_suffix: [BOOLEAN])
Calculations & Deltas
DELTA_DATE(date: [DATE], increase: [BOOLEAN], years: [BIGINT?], months: [BIGINT?], days: [BIGINT?])
DELTA_TIMETZ(timetz: [TIMETZ], increase: [BOOLEAN], hours: [BIGINT?], minutes: [BIGINT?], seconds: [BIGINT?], milliseconds: [BIGINT?])
DELTA_TIMESTAMPTZ(timestamptz: [TIMESTAMPTZ], increase: [BOOLEAN], years: [BIGINT?], months: [BIGINT?], days: [BIGINT?], hours: [BIGINT?], minutes: [BIGINT?], seconds: [BIGINT?], milliseconds: [BIGINT?])
EXTRACT_DATE_DURATION(start_time: [DATE], end_time: [DATE], unit: [Enum:DATE_UNIT])
EXTRACT_TIMETZ_DURATION(start_time: [TIMETZ], end_time: [TIMETZ], unit: [Enum:TIME_UNIT])
EXTRACT_TIMESTAMPTZ_DURATION(start_time: [TIMESTAMPTZ], end_time: [TIMESTAMPTZ], unit: [Enum:TIMESTAMP_UNIT])
Conversions
FROM_TIMESTAMPTZ_TO_DATE(timestamptz: [TIMESTAMPTZ])
FROM_TIMESTAMPTZ_TO_TIMETZ(timestamptz: [TIMESTAMPTZ])
Geography
FROM_COORDINATES(latitude: [DECIMAL], longitude: [DECIMAL])
GEO_DISTANCE(point0: [GEO_POINT], point1: [GEO_POINT], unit: [Enum:GEO_DISTANCE_UNIT])
GEO_LONGITUDE(geo: [GEO_POINT])
GEO_LATITUDE(geo: [GEO_POINT])
Aggregates
SUM(array: [NUMERIC[]])
AVG(array: [NUMERIC[]])
MAX(value0: [COMPARABLE], value1: [COMPARABLE])
MIN(value0: [COMPARABLE], value1: [COMPARABLE])
GREATEST(array: [COMPARABLE[]])
LEAST(array: [COMPARABLE[]])
Element Access
ITEM(array: [ANY[]], index: [BIGINT])
FIRST_ITEM(array: [ANY[]])
LAST_ITEM(array: [ANY[]])
RANDOM_ITEM(array: [ANY[]])
ARRAY_POSITION(array: [ANY[]], item: [ANY])
JSONB
JSON_EXTRACT_BY_DOT_NOTATION_JSONPATH(json: [JSONB], path: [TEXT])
Casting
CAST_FROM_TEXT(value: [TEXT])
CAST_COLUMN_TO_TEXT(value: [ANY])
CAST_ARRAY_TO_TEXT(value: [ANY[]])
CAST_TO_BIGINT(value: [ANY])
CAST_TO_DECIMAL(value: [ANY])
Vector Search
EMBEDDING_VECTOR_DISTANCE(embedded_text_column: [Enum:EMBEDDING_COLUMN], text: [TEXT], distance_function: [Enum:VECTOR_DISTANCE])
System
Explicit Enum Definitions
When an argument requires an [Enum:NAME], you must use one of the following exact string values:
- [Enum:DATE_UNIT]:
YEAR, MONTH, DAY, WEEK
- [Enum:DURATION_UNIT]:
DAY, HOUR, MINUTE, SECOND, MILLISECOND
- [Enum:EMBEDDING_COLUMN]:
(Dynamically generated based on columns with TEXT_COLUMN_VECTOR_SORT extension)
- [Enum:GEO_DISTANCE_UNIT]:
METER, KILOMETER, MILE
- [Enum:LANGUAGE]:
EN, ZH
- [Enum:NUMBER_FORMAT]:
THOUSANDS_SEPARATOR, PERCENT
- [Enum:ROUNDING_MODE]:
HALF_EVEN, HALF_UP, HALF_DOWN, UP, DOWN, CEILING, FLOOR
- [Enum:TIMESTAMP_UNIT]:
YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, MILLISECOND, WEEK
- [Enum:TIME_UNIT]:
HOUR, MINUTE, SECOND, MILLISECOND
- [Enum:VECTOR_DISTANCE]:
EUCLIDEAN, COSINE
Actionflow Protocol
Overview
Although zion-app.functorz.com already support direct CRUD operations that can be initiated from the frontend, many backend operations are multi-step, can be long-running and sometimes have to be asynchronous. Therefore zion-app.functorz.com also supports actionflows for these scenarios. An actionflow is a directed acyclic graph made up of actionflow nodes. These nodes represent either operations (e.g. insert into databsae, invoke another actionflow) or control flow changes (condition and loop). Actionflows also have two special nodes, input and output, where the arguments and return values of the entire actionflow are defined.
Actionflows have two modes of operation, sync or async. A synchronous actionflow is executed within a single database transaction, and therefore when an unexpected error is encountered, will rollback all database changes. Synchronous actionflows have runtime limits to avoid hogging database connection. Asynchronous actionflows run each node inside a new database transaction, so they do not have rollback mechanism, but are more suited for long running tasks, like long http calls, especially those made to LLM APIs as they can take minutes. Within actionflows, all nodes of invoking AI agents built in zion-app.functorz.com natively can only be added inside async ones.
Actionflow invocation process
In order to invoke an actionflow, one needs to obtain its id, a list of arguments and optionally its version. They are found inside the project schema.
Actionflow invocation differ based on their type.
Sync actionflows
Sync actionflows can be invoked via a regular GraphQL mutation. The results will be returned in the response of the same HTTP request.
Request:
mutation someOperationName ($args: Json!) {
fz_invoke_action_flow(actionFlowId: "d3ea4f95-5d34-46e1-b940-91c4028caff5", versionId: 3, args: $args)
}
{
"args": {
"yaml": "post_link:\n url: \"https://zion-app.functorz.com\"\n",
"img_id": 1020000000000111
}
}
Within this query, $args corresponds to the arguments listed in the actionflow's input node.
Response:
{
"data": {
"fz_invoke_action_flow": {
"img": {
"id": 1020000000000090,
"url": "https://fz-zion-static.functorz.com/202510252359/a64a7eb4793728a1977d3ea9e7b7e4e8/project/2000000000521152/import/1110000000000001/image/636.jpg"
},
"url": "https://zion-app.functorz.com"
}
}
}
Async actionflows
Async actionflows are triggered via a GraphQL mutation but the results are not returned in the response of the same HTTP request. Instead, a fz_create_action_flow_task is returned, containing the id of the corresponding task, which is then used for subscribing to the result in a separate GraphQL subscription.
Mutation request:
mutation mh49tgie($args: Json!) {
fz_create_action_flow_task(actionFlowId: "2a9068c5-8ee3-4dad-b3a4-5f3a6d365a2f", versionId: 4, args: $args)
}
{
"args": {
"int": 123,
"img_id": 1020000000000116,
"some_text": "Dreamer",
"datetime_with_timezone": "2025-10-23T20:13:00-07:00"
}
}
Mutation response:
{
"data": {
"fz_create_action_flow_task": 1150000000000148
}
}
Subscription request:
subscription fz_listen_action_flow_result($taskId: Long!) {
fz_listen_action_flow_result(taskId: $taskId) {
__typename"
output
status
}
}
{ "taskId" : 1150000000000148 }
Subscription response:
{
"data": {
"fz_listen_action_flow_result": {
"__typename": "ActionFlowTaskResult",
"output": {
"img": {
"id": 1020000000000089,
"url": "https://fz-zion-static.functorz.com/202510262359/3a5f04371bf68d6c94bb890879101f0a/project/2000000000521152/import/1110000000000001/image/637.jpg"
},
"xyz": {
"type": "Point",
"coordinates": [
131,
22
]
}
},
"status": "COMPLETED"
}
}
}
There might be multiple messages sent by the GraphQL subscription before the final result (inside "output") is returned, and each may contain different status values.
The status field has the following transition rules:
switch (status) {
case CREATED -> Set.of(PROCESSING);
case PROCESSING -> Set.of(COMPLETED, FAILED);
default -> Set.of();
};
AI Agent Protocol
Overview
Zion.app has an integrated AI agent builder, which supports multi-modal (text, video, image) inputs and outputs, prompt templating, context fetching (via database and third-party APIs), tool use (actionflows, third-party APIs and other AI agents) and structured output (JSON according corresponding JSONSchema).
AI Agents' results are delivered differently by the GraphQL service depending on the configuration of its output, namely, whether it is streaming and whether it is structured. A structured output can not be streamed but plain text can be either streamed or not. A structured output must be accompanied by a JSONSchema that describes the JSON's type.
In order to invoke an AI agent, the id and the input arguments must be obtained from the project schema. An AI agent built in Zion.app's agent builder can only be invoked via the GraphQL API asynchronously.
Invocation process for streaming output
An example AI Agent configuration whose output is a streaming plain text will be used to illustrate this process. Its configuration is:
{
"id": "mgzzu8jp",
"summary": "An example summary of what the agent does",
"inputs": {
"mgzzufo2": {
"type": "VIDEO",
"displayName": "the_video",
},
"mh4cjjcf": {
"type": "TEXT",
"displayName": "text",
},
"mh4cjkyv": {
"type": "BIGINT",
"displayName": "some_int",
},
"mh4cjoof": {
"type": "array",
"itemType": "IMAGE",
"displayName": "images",
}
},
"output": "Unstructured Text"
}
- A mutation is sent to start the AI agent, supplying the arguments as inputArgs and the id as zAIConfigId. The response value only contains the id of the corresponding conversation. The keys of inputArgs should be the same keys in the inputs object from the schema. Input parameters of Image / video or other binary assets types, or arrays of such types are handled slightly differently. Their key names wihtin the inputArgs object have
_id suffix. e.g. the following configuration
{
"inputs": {
"mgzzufo2": {
"type": "VIDEO",
"displayName": "the_video",
}
}
}
Corresponds to:
{
"inputArgs": {
"mgzzufo2_id": 1030000000000002,
}
}
Mutation request:
Query:
```gql
mutation ZAICreateConversation($inputArgs: Map_String_ObjectScalar!, $zaiConfigId: String!) {
fz_zai_create_conversation(inputArgs: $inputArgs, zaiConfigId: $zaiConfigId)
}
```
Variables:
```json
{
"inputArgs": {
"mgzzufo2_id": 1030000000000002,
"mh4cjjcf": "Just some text",
"mh4cjkyv": 23,
"mh4cjoof_id": [
1020000000000097,
1020000000000111,
1020000000000120
]
},
"zaiConfigId": "mgzzu8jp"
}
```
Mutation response:
```json
{
"data": {
"fz_zai_create_conversation": 1480
}
}
```
- Using the obtained conversation id to subscribe to the result of the previous invocation of the AI Agent. Multiple messages may be received. The messages' status may transition from IN_PROGRESS to STREAMING to eventually COMPLETED. The last message always gives you COMPLETED status and its data field will contain the consolidated output from all the previous STEAMING messages' data field.
For models that have reasoning content output, it works similarly as the actual output. i.e. Partial reasoning content will be emitted first in multiple messages in the reasoningContent field, and then when everything is ready, the entirety of reasoningContent will be emitted again the COMPLETED message.
Subscription request:
Query: subscription ZaiListenConversationResult($conversationId: Long!) {
fz_zai_listen_conversation_result(conversationId: $conversationId) {
conversationId
status
reasoningContent
images {
id
__typename
}
data
__typename
}
}
Variables: {
"conversationId": 1480
}
Subscription response messages:{
"data": {
"fz_zai_listen_conversation_result": {
"__typename": "ConversationResult",
"conversationId": 1480,
"data": null,
"images": null,
"reasoningContent": null,
"status": "IN_PROGRESS"
}
}
}
{
"data":{
"fz_zai_listen_conversation_result": {
"__typename": "ConversationResult",
"conversationId": 1480,
"data": "This collection features three images and a short video. Two photos show the famous Chinese comedian and actor, Zhao Benshan. A third",
"images": null,
"reasoningContent": null,
"status": "STREAMING"
}
}
}
{
"data":{
"fz_zai_listen_conversation_result": {
"__typename": "ConversationResult",
"conversationId": 1480,
"data": " image is an anime illustration of a young woman in a \"SHOHOKU\" basketball jersey, resembling the character Haruko Akagi from the series *Slam Dunk*.",
"images": null,
"reasoningContent": null,
"status": "STREAMING"
}
}
}
{
"data":{
"fz_zai_listen_conversation_result": {
"__typename": "ConversationResult",
"conversationId": 1480,
"data": "This collection features three images and a short video. Two photos show the famous Chinese comedian and actor, Zhao Benshan. A third image is an anime illustration of a young woman in a \"SHOHOKU\" basketball jersey, resembling the character Haruko Akagi from the series *Slam Dunk*.",
"images": null,
"reasoningContent": null,
"status": "COMPLETED"
}
}
}
Invocation process for non streaming plain text output
The mutation step is identical to the one inside the invocation process for streaming output. I.e. send mutation fz_zai_create_conversation(inputArgs: $inputArgs, zaiConfigId: $zaiConfigId) to obtain the conversation id.
There will be no messages in the STREAMING state. i.e. A message of IN_PROGRESS status will be sent by the server, followed directly by the COMPLETED message with the final result.
Invocation process for AI agents that use models with image output
Certain model support image output, like gemini-2.5-flash-image.
Their invocation process is the same as the plain text ones. Except that in the COMPLETED message, the field images will be filled with content. The images
Their output will be no different from plain-text only outputs, regardless of the streaming setting. Their COMPLETED message looks like:
{
"data": {
"fz_zai_listen_conversation_result": {
"__typename": "ConversationResult",
"conversationId": 1494,
"data": "I merged the three images into one, combining elements from each to create a new, unique image.\n",
"images": [
{
"__typename": "FZ_Image",
"id": 1020000000000164
}
],
"reasoningContent": null,
"status": "COMPLETED"
}
}
}
The ids for FZ_Image in images represent the ids in Zion's file / asset system. Refer to zion-binary-asset-upload-rules
Invocation process for structured output
AI agents with structured output cannot be streaming. They also always come with a JSONSchema in their configuration.
{
"output": {
"type": "object",
"properties": {
"httpLink": {
"type": "string"
},
"reasoning": {
"type": "string"
}
},
"required": [
"httpLink",
"reasoning"
]
}
}
There will be no messages from the GraphQL server that are in the "STREAMING" state. There will be one "COMPLETED" message where the data field is a JSON that satisfies the JSONSchema.
e.g.
{
"httplink": "https://www.google.com/calendar/event?eid=MTcxN2U3cHAzaDFtYTdxYzd0bGV0aHNvYmsgamlhbmd5YW9rYWlqb2huQG0",
"explanation": "No existing events were found on 2025-10-24 in America/Los_Angeles, so there are no conflicts. Preference is mornings; scheduled 08:00–08:10 at Los Altos High school. With no adjacent events, transit checks to previous and next events are trivially satisfied."
}
Continuing conversation
After AI Agent returns result (status = COMPLETED), the conversation can be continued by calling fz_zai_send_ai_message.
The subscription of fz_zai_listen_conversation_result on the same conversationId will continue to receive messages.
e.g.
mutation request:
mutation continue($conversationId: Long!, $text: String) {
fz_zai_send_ai_message(conversationId: $conversationId, text: $text)
}
Variables:
{
"conversationId": 1480,
"text": "make it about the sun"
}
The response from the corresponding fz_zai_listen_conversation_result will then continue. Similar to what happens after one initiates a converation with an AI agent, going through the same IN_PROGRESS -> (STREAMING) -> COMPLETED status transition.
Stopping conversation
For converations still in "IN_PROGRESS" or "STREAMING" states, they can be stopped by calling fz_zai_stop_responding, which always returns true.
When called on conversations with "COMPLETED" state, a 400 error will be thrown inside the errors field of the GraphQL response.
e.g.
mutation request:
mutation continue($conversationId: Long!) {
fz_zai_stop_responding(conversationId: $conversationId)
}
Variables:
{
"conversationId": 1480
}
Binary Asset Upload
All binary assets (images, videos, files) are stored on object storage services (e.g., S3). Their storage path is recorded in Zion's database. When referencing these assets in other tables, you must store only the asset's Zion ID, not its path or URL.
Upload Workflow
To upload a binary asset and obtain its Zion ID, you must follow a strict two-step process:
Step 1: Obtain a Presigned Upload URL
Calculate the MD5 hash of the file (raw 128-bit hash), then Base64-encode it.
Call the appropriate GraphQL mutation to request a presigned upload URL. Use the mutation that matches your asset type:
imagePresignedUrl for images
videoPresignedUrl for videos
filePresignedUrl for other files
Provide:
- The Base64-encoded MD5 hash
- The file format/suffix (see
MediaFormat below)
- (Optional) Access control (see
CannedAccessControlList below)
Example GraphQL Mutations
mutation GetImageUploadUrl($md5: String!, $suffix: MediaFormat!, $acl: CannedAccessControlList) {
imagePresignedUrl(imgMd5Base64: $md5, imageSuffix: $suffix, acl: $acl) {
imageId
uploadUrl
uploadHeaders
}
}
mutation GetVideoUploadUrl($md5: String!, $format: MediaFormat!, $acl: CannedAccessControlList) {
videoPresignedUrl(videoMd5Base64: $md5, videoFormat: $format, acl: $acl) {
videoId
uploadUrl
uploadHeaders
}
}
mutation GetFileUploadUrl($md5: String!, $format: MediaFormat!, $name: String, $suffix: String, $sizeBytes: Int, $acl: CannedAccessControlList) {
filePresignedUrl(
md5Base64: $md5
format: $format
name: $name
suffix: $suffix
sizeBytes: $sizeBytes
acl: $acl
) {
fileId
uploadHeaders
uploadUrl
}
}
CannedAccessControlList (recommended: PRIVATE):
- AUTHENTICATE_READ, AWS_EXEC_READ, BUCKET_OWNER_FULL_CONTROL, BUCKET_OWNER_READ, DEFAULT, LOG_DELIVERY_WRITE, PRIVATE, PUBLIC_READ, PUBLIC_READ_WRITE
MediaFormat:
- CSS, CSV, DOC, DOCX, GIF, HTML, ICO, JPEG, JPG, JSON, MOV, MP3, MP4, OTHER, PDF, PNG, PPT, PPTX, SVG, TXT, WAV, WEBP, XLS, XLSX, XML
Step 2: Upload the File and Use the Returned ID
- The mutation response includes:
- The asset's unique ID (
imageId, videoId, or fileId)
- A presigned
uploadUrl
- Any required
uploadHeaders
- Upload the file:
- Perform an HTTP
PUT request to the uploadUrl with the raw file data
- Include any
uploadHeaders from the mutation response
- Reference the asset:
- Use the returned ID as the value for the corresponding
*_id field in your Zion data mutation (e.g., cover_image_id: returnedImageId)
Note: This two-step process is mandatory for all media uploads in Zion.app.
Third-Party APIs
A project built on Zion.app can have many third-party HTTP APIs imported. These are separated into two categories: query or mutation, roughly (though not always the case) corresponding to the semantics of HTTP GET vs POST.
Each API is stored in the following data structure:
type ScalarType = 'string' | 'boolean' | 'number' | 'integer';
type TypeDefinition =
| ScalarType
| { [key: string]: TypeDefinition | TypeDefinition[] };
interface ThirdPartyApiConfig {
id: string;
name: string;
operation: 'query' | 'mutation';
inputs: { [key: string]: TypeDefinition };
outputs: { [key: string]: TypeDefinition };
}
N.B. The value of the operation field within ThirdPartyApiConfig determines the root GraphQL field. i.e. query -> query operation_${id}, and mutation -> mutation operation_${id}.
Invocation process
Each input should be provided unless the user asks to remove it.
e.g.
Given TPA configuration as follows:
{
"id": "lzb3ownk",
"inputs": {
"body": {
"summary": "string",
"location": "string",
"description": "string",
"start": {
"dateTime": "string",
"timeZone": "string"
},
"end": {
"dateTime": "string",
"timeZone": "string"
},
"attendees": [
"string"
]
},
"Authorization": "string"
},
"outputs": {
"body": {
"kind": "string",
"etag": "string",
"id": "string",
"status": "string",
"htmlLink": "string",
"created": "string",
"updated": "string",
"summary": "string",
"description": "string",
"location": "string",
"creator": {
"email": "string",
"self": "boolean"
},
"organizer": {
"email": "string",
"self": "boolean"
},
"start": {
…(truncated)
1---2name: zion-baas3description: Instructions and authentication code for building headless BaaS applications with Zion.app (functorz.com). Use when integrating Zion backend features like GraphQL, actionflows, AI agents, binary assets, and payments.4---56# Zion.app Headless BaaS Skill78## Overview9This skill outlines how to build frontend applications utilizing Zion.app as a headless Backend-as-a-Service (BaaS). Zion exposes all backend interactions (database, actionflows, third-party APIs, and AI agents) through a single, unified GraphQL API.1011- **HTTP URL**: `https://zion-app.functorz.com/zero/{projectExId}/api/graphql-v2`12- **WebSocket URL**: `wss://zion-app.functorz.com/zero/{projectExId}/api/graphql-subscription`1314## Token Acquisition & Authentication (CRITICAL)15To interact with authenticated endpoints, you must obtain a JWT token by logging in or registering. Unauthenticated requests are assigned an anonymous user role. The JWT can be obtained in two ways. One is via username + password login. The other one is by querying the Meta API & fetching runtime backend token. The token return in FETCH_DATA_VISUALIZER is the runtime backend token. 1617### 1. Username Registration & Login18You should ask user for username and password.19```graphql20mutation AuthenticateWithUsername($username: String!, $password: String!, $register: Boolean!) {21 authenticateWithUsername(username: $username, password: $password, register: $register) {22 account { id, permissionRoles }23 jwt { token }24 }25}26```27*Note: Both mutations return `FZ_Account` which is a subset of the full `account` type. It contains only `email`, `id`, `permissionRoles`, `phoneNumber`, `profileImageUrl`, `roles`, and `username`.*2829## Developer Authentication with Zion.app (Meta API)30If you need to interact directly with the Zion platform (Meta API) to fetch project schemas, list projects, or authenticate as an admin to the runtime backend, follow these steps:3132### 1. Acquire Developer JWT Token33There are two ways to acquire a developer token: via OAuth Flow or via Email/Password login.3435#### Option A: OAuth Flow36Set up a local HTTP server to receive the OAuth callback. Open the Zion authentication endpoint (`https://auth.functorz.com/login`) in the browser and wait for the `token` parameter.3738You can run the bundled authentication script:3940```bash41cd ~/.agents/skills/zion-baas-skill/scripts42npm run auth43```4445#### Option B: Email/Password Login46You can directly login with an email and password using the Meta API:4748```bash49cd ~/.agents/skills/zion-baas-skill/scripts50npm run auth:email <email> <password>51```5253### 2. Querying the Meta API & Fetching Runtime Backend Token54Use the developer JWT token as a Bearer token against the Meta API (`https://zionbackend.functorz.com/api/graphql`) to get the schema or data visualizer tokens. The data visualizer token grants administrative access to the project's runtime backend (`zeroUrl`).5556You can run the bundled script to fetch the token. It requires the developer token to be present in `.zion/credentials.yaml`.5758```bash59cd ~/.agents/skills/zion-baas-skill/scripts60npm run fetch-token -- <projectExId>61```6263You can also search for projects or fetch their schema via the Meta API using the bundled `meta` script:6465```bash66cd ~/.agents/skills/zion-baas-skill/scripts67# Search projects (returns project names and exIds)68npm run meta -- search-projects "optional search term"6970# Fetch project schema (returns data models, actionflows, apis)71npm run meta -- fetch-schema <projectExId>72```7374## Credential Management & State Persistence7576All credentials and project state MUST be persisted in the `.zion` directory at the root of the user's project in YAML format, typically in a file named `.zion/credentials.yaml`.7778### Required YAML Format7980The credentials file must adhere to the following structure:8182```yaml83# .zion/credentials.yaml84developer_token:85 token: "<your_developer_jwt_token>" # Used to communicate with zionbackend.functorz.com86 expiry: "<timestamp_or_date_of_expiry>"8788project:89 exId: "<project_ex_id>"90 name: "<project_name>"91 admin_token:92 token: "<runtime_backend_admin_token>"93 expiry: "<timestamp_or_date_of_expiry>"94 other_users:95 - user_id: "<user_id>"96 user_tags: 97 - "<notable_information_about_the_user_1>"98 token:99 token: "<user_jwt_token>"100 expiry: "<timestamp_or_date_of_expiry>"101```102103- **`developer_token`**: The JWT token acquired via the OAuth flow, used against the Meta API (`zionbackend.functorz.com`).104- **`project`**: Contains the core project identifiers (`exId`, `name`), the `admin_token` (data visualizer token) for the runtime backend, and `other_users`.105- **`other_users`**: A list of authenticated users (e.g., test accounts) containing their `user_id`, helpful `user_tags` to identify their purpose, and their `token`.106- **`expiry`**: All stored tokens must include an associated expiry.107108## Executing GraphQL Queries & Subscriptions via CLI109You can use the bundled scripts to quickly test GraphQL queries, mutations, and subscriptions from the command line without writing frontend boilerplate. These scripts automatically read the correct token from your project's `.zion/credentials.yaml`.110111### 1. Execute a Query or Mutation112Pass your GraphQL query or mutation as a string.113114```bash115cd ~/.agents/skills/zion-baas-skill/scripts116npm run gql -- <projectExId> <role> '<query_string>' '<optional_variables_json>'117```118- `<role>`: Can be `admin` (uses data visualizer token), `anonymous` (no token), or a specific `user_id` (fetches token from `other_users` in `.zion/credentials.yaml`).119120*Example:*121```bash122npm run gql -- myProjectEx123 admin 'query GetProject($id: Int!) { project(id: $id) { name } }' '{"id": 1}'123```124125### 2. Listen to a Subscription126Pass your GraphQL subscription as a string.127128```bash129cd ~/.agents/skills/zion-baas-skill/scripts130npm run subscribe -- <projectExId> <role> '<query_string>' '<optional_variables_json>'131```132The script will establish a WebSocket connection and continuously print events as they arrive until you kill it (Ctrl+C).133134135## Database & GraphQL Schema Rules136- Use `zion MCP server` or `webfetch/curl` to introspect the schema before making assumptions.137138The GraphQL schema is automatically generated directly from the PostgreSQL data model.139140> **CRITICAL CATCH-ALL RULE:**141> If you are ever in doubt about the exact structure, available fields, Enum values (like `[Enum:ROUNDING_MODE]`), or specific arguments for an endpoint, **you must use the webfetch/curl tool to introspect the live GraphQL schema** via the provided endpoint URL before making assumptions.142 143144### 1. Naming Conventions & Root Operations145146Each table generates corresponding root query, mutation, and subscription fields. For a table named `[table]`:147148#### Queries149* `[table]`: Fetch lists (supports `where`, `order_by`, `distinct_on`, `limit`, `offset`).150* `[table]_by_pk`: Fetch single record by primary key (`id`).151* `[table]_aggregate`: Aggregate queries (`count`, `sum`, `avg`, `min`, `max`).152* `[table]_group_by`: Group records based on specified fields.153* `fz_[table]_by_[column]`: Auto-generated spatial proximity search if the table contains a `geo_point` column.154* **Relay API**: If configured, generates a cursor-based pagination query returning a `Connection_[table]` object.155156#### Mutations157* `insert_[table]`, `insert_[table]_one`: Create records. Supports nested inserts and `on_conflict` (requires `constraint` and `update_columns`).158* `update_[table]`, `update_[table]_by_pk`: Modify records. Uses `_set` (replace) and `_inc` (increment numeric). `where` is required (`!`) for bulk updates. *(Note: Hasura JSONB update operators like `_append`/`_delete_key` are not supported).*159* `delete_[table]`, `delete_[table]_by_pk`: Remove records. `where` is required (`!`) for bulk deletes.160* `export_[table]`: Trigger a data export task for the table.161162#### Subscriptions163* `[table]`, `[table]_by_pk`, `[table]_aggregate`: Live queries mirroring their standard query counterparts.164165### 2. Column Types and Data Mappings166167#### Primitive Types168* `text` → `String`, `integer` → `Int`, `bigint`, `bigserial` → `bigint`, `float8` → `Float8`, `decimal` → `Decimal`, `boolean` → `Boolean`, `jsonb` → `jsonb`.169* **Time/Date**: `timestamptz`, `timetz`, `date`, `interval`.170* **Geo**: `geo_point` → `geography`.171172#### Composite (Media) Types173Media columns (`image`, `file`, `video`) are structurally 1:N relations but act as fields. 174* **Single Media**: Stored as `[column]_id` (e.g., `cover_image_id`) referencing system asset tables (`FZ_Image`, `FZ_File`, `FZ_Video`).175* **Media Lists**: Types like `image_list`, `video_list`, `file_list` map to GraphQL Arrays and are stored as `[column]_ids` (e.g., `gallery_images_ids`).176177#### System-Managed Columns178`id`, `created_at`, `updated_at` are read-only and automatically managed. They cannot be used in mutation inputs (`_set`, `_inc`, insert inputs).179180### 3. Relationships181182Relationships are defined by foreign keys and determine GraphQL nested fields:183* **1:1 (One-to-One)**: Yields a single nested object (e.g., `meta: post_meta`).184* **1:N (One-to-Many)**: Yields an array (e.g., `post_tags: [post_tag]`) and an aggregate object (e.g., `post_tags_aggregate`).185186### 4. Filtering (`where` clauses)187188Filters rely on `[table]_bool_exp`.189190#### Logical Operators191* `_and: [bool_exp]`, `_or: [bool_exp]`, `_not: bool_exp`192193#### Relation Filters194Navigate relationships using the relationship field name directly. The value is a nested `[related_table]_bool_exp` object.195* **To-One Relationships (1:1, N:1)**: Filters the parent record based on the single related record's fields.196 *(e.g., Find posts where the author's name is "John": `author: { name: { _eq: ... } }`)*197* **To-Many Relationships (1:N, N:M)**: Uses `EXISTS` semantics. The parent record is returned if **any** related record in the array matches the nested condition.198 *(e.g., Find posts that have at least one tag named "Tech": `post_tags: { tag: { name: { _eq: ... } } }`)*199200#### Comparison Predicates (Strict Pattern)201Zion uses a strict **Operator-First Pattern**. A predicate must start with the operator. If it's a generic operator, the operand wrapper type must match the *final evaluated type*, not necessarily the column type.202203**Structure:**204```json205{206 "_operator": {207 "operand_type": {208 "left_operand": { ... },209 "right_operand": { ... }210 }211 }212}213```214215* **Operators:** 216 * **Comparison (Generic):** `_eq`, `_neq`, `_gt`, `_lt`, `_gte`, `_lte`217 * **Array (Generic):** `_in`, `_nin`218 * **Nullity (Generic, Unary):** `_is_null`, `_is_not_null`219 * **Text (String Pattern):** `_like`, `_nlike`, `_ilike`, `_nilike`, `_similar`, `_nsimilar`220 * **JSONB:** `_contains`, `_contained_in`, `_has_key`, `_has_keys_any`, `_has_keys_all`221 * **Boolean:** `_is_true`, `_is_false`222 * **Collection:** `_is_empty`, `_is_not_empty`223* **Operand Definitions (`left_operand` / `right_operand`):**224 * **Literal**: `{"literal": value}`225 * **Column**: `{"column": "field_name"}`226 * **Function**: `{"function_name": { ...args }}`227* **Operand Types (Determined by Final Value):** `bigint_operand`, `text_operand`, `boolean_operand`, `timestamptz_operand`, etc.228229**Example Predicate (Extract Month from Timestamp and check if = 12):**230```json231{232 "_eq": {233 "bigint_operand": {234 "left_operand": {235 "extract_timestamptz": { "time": { "column": "created_at" }, "unit": "MONTH" }236 },237 "right_operand": { "literal": "12" }238 }239 }240}241```242243### 5. Aggregations, Window Functions, and Order By244245#### Aggregation (`[table]_aggregate`)246Returns an aggregate query object containing two main fields:247* `nodes`: An array of the actual table objects (`[table!]!`) containing the raw data rows that match the query's `where`, `limit`, `offset`, and `order_by` criteria.248* `aggregate`: An object containing statistical calculations over the matched rows:249 * `count(columns: [Enum], distinct: Boolean)`: 250 * If no columns are provided, it counts all rows (`COUNT(*)`).251 * If `distinct: true` and 1+ columns are provided, it counts unique values or unique combinations of the specified columns.252 * If `distinct: false` and multiple columns are provided, the system restricts the count to only evaluate the *first* column in the array.253 * `sum`, `avg`: Only available for **numeric** columns.254 * `max`, `min`: Available for **comparable** columns (numeric, time, text).255256#### Window Functions257Advanced analytical operations like `ROW_NUMBER`, `RANK`, `DENSE_RANK`, `NTH_VALUE` are supported in specific formula and window frame inputs.258259#### Sorting (`order_by`)260Supports sorting by:2611. Direct columns: `{ title: asc }`2622. Related 1:1 records: `{ author: { name: desc } }`2633. Aggregates of N:1 records: `{ post_tags_aggregate: { count: desc } }`2644. Vector Search: Text columns may support similarity sorting if the `TEXT_COLUMN_VECTOR_SORT` extension is applied.265266### 6. Formula Functions (Operands)267Functions are used inside operand wrappers by wrapping the uppercase function name around its arguments (e.g., `{"EXTRACT_TIMESTAMPTZ": { "time": ..., "unit": ... }}`).268269#### Input Constraints & Semantics270* **[TYPE]**: Indicates a required column or scalar operand of that exact type.271* **[TYPE?]**: Indicates an optional operand (usually defaults to `0` or `null`).272* **[ANY]**: Accepts any column or scalar operand of any type.273* **[NUMERIC]**: Accepts `BIGINT`, `INTEGER`, `DECIMAL`, `FLOAT8`, or `BIGSERIAL`.274* **[COMPARABLE]**: Accepts `NUMERIC` types, `TEXT`, `DATE`, `TIMESTAMPTZ`, `TIMETZ`, or `INTERVAL`.275* **[ANY[]]**: Accepts an array operand of any type.276* **[Enum:NAME]**: Requires an explicit Enum value matching the `[NAME]` definition (e.g., `[Enum:DATE_UNIT]` requires `YEAR`, `MONTH`, etc.).277* **Nested Functions**: Arguments can often be the output of other functions, provided the output type matches the expected input type.278279#### Manipulation280* `CONCAT(items: [TEXT[]])`281* `SUBSTRING(source_text: [TEXT], start_index: [BIGINT], end_index: [BIGINT])`282* `LEFT(source_text: [TEXT], length: [BIGINT])`283* `RIGHT(source_text: [TEXT], length: [BIGINT])`284* `LOWER(source_text: [TEXT])`285* `UPPER(source_text: [TEXT])`286* `TRIM(text: [TEXT])`287* `TRIM_TRAILING_ZERO(source_text: [TEXT])`288* `REPEAT(text: [TEXT], times: [BIGINT])`289* `ENCODE_URL(text: [TEXT])`290* `DECODE_URL(text: [TEXT])`291* `ARRAY_CONCAT(first_array: [ANY[]], second_array: [ANY[]])`292* `SLICE(array: [ANY[]], start_index: [BIGINT], length: [BIGINT])`293* `UNIQUE(array: [ANY[]])`294* `COALESCE(array: [ANY[]])`295296#### Search & Replace297* `REPLACE_OCCURRENCES(source_text: [TEXT], search_text: [TEXT], replace_text: [TEXT], max_replacements: [BIGINT])`298* `REPLACE_AT_POSITION(source_text: [TEXT], start_index: [BIGINT], length: [BIGINT], replace_text: [TEXT])`299* `POSITION(source_text: [TEXT], search_text: [TEXT])`300* `CONTAINS(source_text: [TEXT], search_text: [TEXT])`301302#### Regex303* `REGEX_EXTRACT(text: [TEXT], regex: [TEXT])`304* `REGEX_REPLACE(text: [TEXT], regex: [TEXT], replacement: [TEXT])`305* `REGEX_EXTRACT_ALL(text: [TEXT], regex: [TEXT])`306* `REGEX_MATCH(text: [TEXT], regex: [TEXT])`307308#### Formatting & Utils309* `TEXT_DECIMAL_FORMAT(number: [DECIMAL], fraction_digits: [BIGINT], rounding_mode: [Enum:ROUNDING_MODE], clear_trailing_zeros: [BOOLEAN])`310* `NUMBER_FORMAT(number: [DECIMAL], fraction_digits: [BIGINT], format: [Enum:NUMBER_FORMAT])`311* `STRING_LEN(source_text: [TEXT])`312* `RANDOM_TEXT(min_length: [BIGINT], max_length: [BIGINT], include_numbers: [BOOLEAN], include_lower_case: [BOOLEAN], include_upper_case: [BOOLEAN])`313* `UUID()`314* `JOIN(array: [TEXT[]], separator: [TEXT])`315* `SPLIT(source_text: [TEXT], delimiter: [TEXT])`316* `ARRAY_LEN(array: [ANY[]])`317318#### Arithmetic319* `ADD(value0: [NUMERIC], value1: [NUMERIC])`320* `SUBTRACT(minuend: [NUMERIC], subtrahend: [NUMERIC])`321* `MULTIPLY(value0: [NUMERIC], value1: [NUMERIC])`322* `DIVIDE(dividend: [DECIMAL], divisor: [DECIMAL])`323* `MODULO(dividend: [NUMERIC], divisor: [NUMERIC])`324* `ABS(number: [NUMERIC])`325* `POW(base: [NUMERIC], exponent: [NUMERIC])`326* `LOG(base: [DECIMAL], argument: [DECIMAL])`327328#### Rounding & Formats329* `ROUND_UP(number: [DECIMAL])`330* `ROUND_DOWN(number: [DECIMAL])`331* `DECIMAL_FORMAT(number: [DECIMAL], fraction_digits: [BIGINT], rounding_mode: [Enum:ROUNDING_MODE])`332333#### Generators334* `RANDOM_BIGINT(min_length: [BIGINT], max_length: [BIGINT])`335* `SEQUENCE(start: [BIGINT], end: [BIGINT], step: [BIGINT])`336337#### Current Time338* `CURRENT_DATE()`339* `CURRENT_TIMETZ()`340* `CURRENT_TIMESTAMPTZ()`341342#### Constructors343* `MAKE_DATE(years: [BIGINT], months: [BIGINT], days: [BIGINT])`344* `MAKE_TIMETZ(hours: [BIGINT?], minutes: [BIGINT?], seconds: [BIGINT?], milliseconds: [BIGINT?])`345* `MAKE_TIMESTAMPTZ(years: [BIGINT], months: [BIGINT], days: [BIGINT], hours: [BIGINT?], minutes: [BIGINT?], seconds: [BIGINT?], milliseconds: [BIGINT?])`346* `MAKE_INTERVAL(years: [BIGINT], months: [BIGINT], weeks: [BIGINT], days: [BIGINT], hours: [BIGINT], minutes: [BIGINT], seconds: [BIGINT], milliseconds: [BIGINT])`347* `FROM_DATE_AND_TIMETZ(date: [DATE], timetz: [TIMETZ])`348349#### Extraction & Formatting350* `EXTRACT_DATE(time: [DATE], unit: [Enum:DATE_UNIT])`351* `EXTRACT_TIMETZ(time: [TIMETZ], unit: [Enum:TIME_UNIT])`352* `EXTRACT_TIMESTAMPTZ(time: [TIMESTAMPTZ], unit: [Enum:TIMESTAMP_UNIT])`353* `DATE_FORMAT(time: [DATE], format: [TEXT], language: [Enum:LANGUAGE])`354* `TIMETZ_FORMAT(time: [TIMETZ], format: [TEXT], language: [Enum:LANGUAGE])`355* `TIMESTAMPTZ_FORMAT(time: [TIMESTAMPTZ], format: [TEXT], language: [Enum:LANGUAGE])`356* `DURATION_FORMAT(duration: [DECIMAL], unit: [Enum:DURATION_UNIT], format: [TEXT])`357* `RELATIVE_DATE(time: [DATE], language: [Enum:LANGUAGE], hide_suffix: [BOOLEAN])`358* `RELATIVE_TIMESTAMPTZ(time: [TIMESTAMPTZ], language: [Enum:LANGUAGE], hide_suffix: [BOOLEAN])`359360#### Calculations & Deltas361* `DELTA_DATE(date: [DATE], increase: [BOOLEAN], years: [BIGINT?], months: [BIGINT?], days: [BIGINT?])`362* `DELTA_TIMETZ(timetz: [TIMETZ], increase: [BOOLEAN], hours: [BIGINT?], minutes: [BIGINT?], seconds: [BIGINT?], milliseconds: [BIGINT?])`363* `DELTA_TIMESTAMPTZ(timestamptz: [TIMESTAMPTZ], increase: [BOOLEAN], years: [BIGINT?], months: [BIGINT?], days: [BIGINT?], hours: [BIGINT?], minutes: [BIGINT?], seconds: [BIGINT?], milliseconds: [BIGINT?])`364* `EXTRACT_DATE_DURATION(start_time: [DATE], end_time: [DATE], unit: [Enum:DATE_UNIT])`365* `EXTRACT_TIMETZ_DURATION(start_time: [TIMETZ], end_time: [TIMETZ], unit: [Enum:TIME_UNIT])`366* `EXTRACT_TIMESTAMPTZ_DURATION(start_time: [TIMESTAMPTZ], end_time: [TIMESTAMPTZ], unit: [Enum:TIMESTAMP_UNIT])`367368#### Conversions369* `FROM_TIMESTAMPTZ_TO_DATE(timestamptz: [TIMESTAMPTZ])`370* `FROM_TIMESTAMPTZ_TO_TIMETZ(timestamptz: [TIMESTAMPTZ])`371372#### Geography373* `FROM_COORDINATES(latitude: [DECIMAL], longitude: [DECIMAL])`374* `GEO_DISTANCE(point0: [GEO_POINT], point1: [GEO_POINT], unit: [Enum:GEO_DISTANCE_UNIT])`375* `GEO_LONGITUDE(geo: [GEO_POINT])`376* `GEO_LATITUDE(geo: [GEO_POINT])`377378#### Aggregates379* `SUM(array: [NUMERIC[]])`380* `AVG(array: [NUMERIC[]])`381* `MAX(value0: [COMPARABLE], value1: [COMPARABLE])`382* `MIN(value0: [COMPARABLE], value1: [COMPARABLE])`383* `GREATEST(array: [COMPARABLE[]])`384* `LEAST(array: [COMPARABLE[]])`385386#### Element Access387* `ITEM(array: [ANY[]], index: [BIGINT])`388* `FIRST_ITEM(array: [ANY[]])`389* `LAST_ITEM(array: [ANY[]])`390* `RANDOM_ITEM(array: [ANY[]])`391* `ARRAY_POSITION(array: [ANY[]], item: [ANY])`392393#### JSONB394* `JSON_EXTRACT_BY_DOT_NOTATION_JSONPATH(json: [JSONB], path: [TEXT])`395396#### Casting397* `CAST_FROM_TEXT(value: [TEXT])`398* `CAST_COLUMN_TO_TEXT(value: [ANY])`399* `CAST_ARRAY_TO_TEXT(value: [ANY[]])`400* `CAST_TO_BIGINT(value: [ANY])`401* `CAST_TO_DECIMAL(value: [ANY])`402403#### Vector Search404* `EMBEDDING_VECTOR_DISTANCE(embedded_text_column: [Enum:EMBEDDING_COLUMN], text: [TEXT], distance_function: [Enum:VECTOR_DISTANCE])`405406#### System407* `NULL_VALUE()`408409#### Explicit Enum Definitions410When an argument requires an `[Enum:NAME]`, you must use one of the following exact string values:411* **[Enum:DATE_UNIT]**: `YEAR, MONTH, DAY, WEEK`412* **[Enum:DURATION_UNIT]**: `DAY, HOUR, MINUTE, SECOND, MILLISECOND`413* **[Enum:EMBEDDING_COLUMN]**: `(Dynamically generated based on columns with TEXT_COLUMN_VECTOR_SORT extension)`414* **[Enum:GEO_DISTANCE_UNIT]**: `METER, KILOMETER, MILE`415* **[Enum:LANGUAGE]**: `EN, ZH`416* **[Enum:NUMBER_FORMAT]**: `THOUSANDS_SEPARATOR, PERCENT`417* **[Enum:ROUNDING_MODE]**: `HALF_EVEN, HALF_UP, HALF_DOWN, UP, DOWN, CEILING, FLOOR`418* **[Enum:TIMESTAMP_UNIT]**: `YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, MILLISECOND, WEEK`419* **[Enum:TIME_UNIT]**: `HOUR, MINUTE, SECOND, MILLISECOND`420* **[Enum:VECTOR_DISTANCE]**: `EUCLIDEAN, COSINE`421422## Actionflow Protocol423424### Overview425Although zion-app.functorz.com already support direct CRUD operations that can be initiated from the frontend, many backend operations are multi-step, can be long-running and sometimes have to be asynchronous. Therefore zion-app.functorz.com also supports actionflows for these scenarios. An actionflow is a directed acyclic graph made up of actionflow nodes. These nodes represent either operations (e.g. insert into databsae, invoke another actionflow) or control flow changes (condition and loop). Actionflows also have two special nodes, input and output, where the arguments and return values of the entire actionflow are defined. 426427Actionflows have two modes of operation, sync or async. A synchronous actionflow is executed within a single database transaction, and therefore when an unexpected error is encountered, will rollback all database changes. Synchronous actionflows have runtime limits to avoid hogging database connection. Asynchronous actionflows run each node inside a new database transaction, so they do not have rollback mechanism, but are more suited for long running tasks, like long http calls, especially those made to LLM APIs as they can take minutes. Within actionflows, all nodes of invoking AI agents built in zion-app.functorz.com natively can only be added inside async ones. 428429### Actionflow invocation process430In order to invoke an actionflow, one needs to obtain its id, a list of arguments and optionally its version. They are found inside the project schema. 431Actionflow invocation differ based on their type. 432433#### Sync actionflows434Sync actionflows can be invoked via a regular GraphQL mutation. The results will be returned in the response of the same HTTP request. 435Request:436```gql437mutation someOperationName ($args: Json!) {438 fz_invoke_action_flow(actionFlowId: "d3ea4f95-5d34-46e1-b940-91c4028caff5", versionId: 3, args: $args)439}440```441442```json443{444 "args": {445 "yaml": "post_link:\n url: \"https://zion-app.functorz.com\"\n",446 "img_id": 1020000000000111447 }448}449```450Within this query, $args corresponds to the arguments listed in the actionflow's input node. 451Response:452```json453{454 "data": {455 "fz_invoke_action_flow": {456 "img": {457 "id": 1020000000000090,458 "url": "https://fz-zion-static.functorz.com/202510252359/a64a7eb4793728a1977d3ea9e7b7e4e8/project/2000000000521152/import/1110000000000001/image/636.jpg"459 },460 "url": "https://zion-app.functorz.com"461 }462 }463}464```465466#### Async actionflows467Async actionflows are triggered via a GraphQL mutation but the results are not returned in the response of the same HTTP request. Instead, a fz_create_action_flow_task is returned, containing the id of the corresponding task, which is then used for subscribing to the result in a separate GraphQL subscription. 468Mutation request:469```gql470mutation mh49tgie($args: Json!) {471 fz_create_action_flow_task(actionFlowId: "2a9068c5-8ee3-4dad-b3a4-5f3a6d365a2f", versionId: 4, args: $args)472}473```474475```json476{477 "args": {478 "int": 123,479 "img_id": 1020000000000116,480 "some_text": "Dreamer",481 "datetime_with_timezone": "2025-10-23T20:13:00-07:00"482 }483}484```485Mutation response:486{487 "data": {488 "fz_create_action_flow_task": 1150000000000148489 }490}491492Subscription request: 493```gql494subscription fz_listen_action_flow_result($taskId: Long!) {495 fz_listen_action_flow_result(taskId: $taskId) {496 __typename"497 output498 status499 }500}501```502```json503{ "taskId" : 1150000000000148 }504```505Subscription response:506```json507{508 "data": {509 "fz_listen_action_flow_result": {510 "__typename": "ActionFlowTaskResult",511 "output": {512 "img": {513 "id": 1020000000000089,514 "url": "https://fz-zion-static.functorz.com/202510262359/3a5f04371bf68d6c94bb890879101f0a/project/2000000000521152/import/1110000000000001/image/637.jpg"515 },516 "xyz": {517 "type": "Point",518 "coordinates": [519 131,520 22521 ]522 }523 },524 "status": "COMPLETED"525 }526 }527}528```529There might be multiple messages sent by the GraphQL subscription before the final result (inside "output") is returned, and each may contain different status values. 530The status field has the following transition rules:531```java532switch (status) {533 case CREATED -> Set.of(PROCESSING);534 case PROCESSING -> Set.of(COMPLETED, FAILED);535 default -> Set.of();536};537```538539## AI Agent Protocol540541### Overview542Zion.app has an integrated AI agent builder, which supports multi-modal (text, video, image) inputs and outputs, prompt templating, context fetching (via database and third-party APIs), tool use (actionflows, third-party APIs and other AI agents) and structured output (JSON according corresponding JSONSchema). 543AI Agents' results are delivered differently by the GraphQL service depending on the configuration of its output, namely, whether it is streaming and whether it is structured. A structured output can not be streamed but plain text can be either streamed or not. A structured output must be accompanied by a JSONSchema that describes the JSON's type. 544In order to invoke an AI agent, the id and the input arguments must be obtained from the project schema. An AI agent built in Zion.app's agent builder can only be invoked via the GraphQL API asynchronously. 545546547### Invocation process for streaming output548An example AI Agent configuration whose output is a streaming plain text will be used to illustrate this process. Its configuration is: 549```json550{551 "id": "mgzzu8jp",552 "summary": "An example summary of what the agent does",553 "inputs": {554 "mgzzufo2": {555 "type": "VIDEO",556 "displayName": "the_video",557 },558 "mh4cjjcf": {559 "type": "TEXT",560 "displayName": "text",561 },562 "mh4cjkyv": {563 "type": "BIGINT",564 "displayName": "some_int",565 },566 "mh4cjoof": {567 "type": "array",568 "itemType": "IMAGE",569 "displayName": "images",570 }571 },572 "output": "Unstructured Text"573}574```5751. A mutation is sent to start the AI agent, supplying the arguments as inputArgs and the id as zAIConfigId. The response value only contains the id of the corresponding conversation. The keys of inputArgs should be the same keys in the inputs object from the schema. Input parameters of Image / video or other binary assets types, or arrays of such types are handled slightly differently. Their key names wihtin the inputArgs object have `_id` suffix. e.g. the following configuration 576```json577{ 578 "inputs": {579 "mgzzufo2": {580 "type": "VIDEO",581 "displayName": "the_video",582 }583 }584}585```586Corresponds to:587```json588{589 "inputArgs": {590 "mgzzufo2_id": 1030000000000002,591 }592}593``` 594595 Mutation request: 596 Query:597 ```gql598 mutation ZAICreateConversation($inputArgs: Map_String_ObjectScalar!, $zaiConfigId: String!) {599 fz_zai_create_conversation(inputArgs: $inputArgs, zaiConfigId: $zaiConfigId)600 }601 ```602 Variables:603 ```json604 {605 "inputArgs": {606 "mgzzufo2_id": 1030000000000002,607 "mh4cjjcf": "Just some text",608 "mh4cjkyv": 23,609 "mh4cjoof_id": [610 1020000000000097,611 1020000000000111,612 1020000000000120613 ]614 },615 "zaiConfigId": "mgzzu8jp"616 }617 ```618 Mutation response:619 ```json620 {621 "data": {622 "fz_zai_create_conversation": 1480623 }624 }625 ```6262. Using the obtained conversation id to subscribe to the result of the previous invocation of the AI Agent. Multiple messages may be received. The messages' status may transition from IN_PROGRESS to STREAMING to eventually COMPLETED. The last message always gives you COMPLETED status and its data field will contain the consolidated output from all the previous STEAMING messages' data field. 627For models that have reasoning content output, it works similarly as the actual output. i.e. Partial reasoning content will be emitted first in multiple messages in the reasoningContent field, and then when everything is ready, the entirety of reasoningContent will be emitted again the COMPLETED message. 628 Subscription request: 629 Query: 630 ```gql631 subscription ZaiListenConversationResult($conversationId: Long!) {632 fz_zai_listen_conversation_result(conversationId: $conversationId) {633 conversationId634 status635 reasoningContent636 images {637 id638 __typename639 }640 data641 __typename642 }643 }644 ```645 Variables: 646 ```json647 {648 "conversationId": 1480649 }650 ```651 Subscription response messages:652 ```json653 {654 "data": {655 "fz_zai_listen_conversation_result": {656 "__typename": "ConversationResult",657 "conversationId": 1480,658 "data": null,659 "images": null,660 "reasoningContent": null,661 "status": "IN_PROGRESS"662 }663 }664 }665 ```666 ```json667 {668 "data":{669 "fz_zai_listen_conversation_result": {670 "__typename": "ConversationResult",671 "conversationId": 1480,672 "data": "This collection features three images and a short video. Two photos show the famous Chinese comedian and actor, Zhao Benshan. A third",673 "images": null,674 "reasoningContent": null,675 "status": "STREAMING"676 }677 }678 }679 ```680 ```json681 {682 "data":{683 "fz_zai_listen_conversation_result": {684 "__typename": "ConversationResult",685 "conversationId": 1480,686 "data": " image is an anime illustration of a young woman in a \"SHOHOKU\" basketball jersey, resembling the character Haruko Akagi from the series *Slam Dunk*.",687 "images": null,688 "reasoningContent": null,689 "status": "STREAMING"690 }691 }692 }693 ```694 ```json695 {696 "data":{697 "fz_zai_listen_conversation_result": {698 "__typename": "ConversationResult",699 "conversationId": 1480,700 "data": "This collection features three images and a short video. Two photos show the famous Chinese comedian and actor, Zhao Benshan. A third image is an anime illustration of a young woman in a \"SHOHOKU\" basketball jersey, resembling the character Haruko Akagi from the series *Slam Dunk*.",701 "images": null,702 "reasoningContent": null,703 "status": "COMPLETED"704 }705 }706 }707 ```708### Invocation process for non streaming plain text output7091. The mutation step is identical to the one inside the invocation process for streaming output. I.e. send mutation fz_zai_create_conversation(inputArgs: $inputArgs, zaiConfigId: $zaiConfigId) to obtain the conversation id. 7107112. There will be no messages in the STREAMING state. i.e. A message of IN_PROGRESS status will be sent by the server, followed directly by the COMPLETED message with the final result. 712713### Invocation process for AI agents that use models with image output714Certain model support image output, like gemini-2.5-flash-image. 715Their invocation process is the same as the plain text ones. Except that in the COMPLETED message, the field images will be filled with content. The images716Their output will be no different from plain-text only outputs, regardless of the streaming setting. Their COMPLETED message looks like:717```json718{719 "data": {720 "fz_zai_listen_conversation_result": {721 "__typename": "ConversationResult",722 "conversationId": 1494,723 "data": "I merged the three images into one, combining elements from each to create a new, unique image.\n",724 "images": [725 {726 "__typename": "FZ_Image",727 "id": 1020000000000164728 }729 ],730 "reasoningContent": null,731 "status": "COMPLETED"732 }733 }734}735```736The ids for FZ_Image in images represent the ids in Zion's file / asset system. Refer to zion-binary-asset-upload-rules737738739### Invocation process for structured output740AI agents with structured output cannot be streaming. They also always come with a JSONSchema in their configuration. 741```json742{743 "output": {744 "type": "object",745 "properties": {746 "httpLink": {747 "type": "string"748 },749 "reasoning": {750 "type": "string"751 }752 },753 "required": [754 "httpLink",755 "reasoning"756 ]757 }758}759```760There will be no messages from the GraphQL server that are in the "STREAMING" state. There will be one "COMPLETED" message where the data field is a JSON that satisfies the JSONSchema. 761e.g. 762```json763{764 "httplink": "https://www.google.com/calendar/event?eid=MTcxN2U3cHAzaDFtYTdxYzd0bGV0aHNvYmsgamlhbmd5YW9rYWlqb2huQG0",765 "explanation": "No existing events were found on 2025-10-24 in America/Los_Angeles, so there are no conflicts. Preference is mornings; scheduled 08:00–08:10 at Los Altos High school. With no adjacent events, transit checks to previous and next events are trivially satisfied."766}767```768769### Continuing conversation770After AI Agent returns result (status = COMPLETED), the conversation can be continued by calling fz_zai_send_ai_message. 771The subscription of fz_zai_listen_conversation_result on the same conversationId will continue to receive messages. 772e.g. 773 mutation request: 774 ```gql775 mutation continue($conversationId: Long!, $text: String) {776 fz_zai_send_ai_message(conversationId: $conversationId, text: $text)777 }778 ```779 Variables: 780 ```json781 {782 "conversationId": 1480,783 "text": "make it about the sun"784 }785 ```786The response from the corresponding fz_zai_listen_conversation_result will then continue. Similar to what happens after one initiates a converation with an AI agent, going through the same IN_PROGRESS -> (STREAMING) -> COMPLETED status transition. 787788### Stopping conversation789For converations still in "IN_PROGRESS" or "STREAMING" states, they can be stopped by calling fz_zai_stop_responding, which always returns true. 790When called on conversations with "COMPLETED" state, a 400 error will be thrown inside the `errors` field of the GraphQL response. 791e.g. 792 mutation request: 793 ```gql794 mutation continue($conversationId: Long!) {795 fz_zai_stop_responding(conversationId: $conversationId)796 }797 ```798 Variables: 799 ```json800 {801 "conversationId": 1480802 }803 ```804805## Binary Asset Upload806All binary assets (images, videos, files) are stored on object storage services (e.g., S3). Their storage path is recorded in Zion's database. **When referencing these assets in other tables, you must store only the asset's Zion ID, not its path or URL.**807808### Upload Workflow809To upload a binary asset and obtain its Zion ID, you must follow a strict two-step process:810811#### Step 1: Obtain a Presigned Upload URL8121. **Calculate the MD5 hash** of the file (raw 128-bit hash), then Base64-encode it.8132. **Call the appropriate GraphQL mutation** to request a presigned upload URL. Use the mutation that matches your asset type:814815 - `imagePresignedUrl` for images816 - `videoPresignedUrl` for videos817 - `filePresignedUrl` for other files818819 Provide:820 - The Base64-encoded MD5 hash821 - The file format/suffix (see `MediaFormat` below)822 - (Optional) Access control (see `CannedAccessControlList` below)823824 ##### Example GraphQL Mutations825 ```graphql826 mutation GetImageUploadUrl($md5: String!, $suffix: MediaFormat!, $acl: CannedAccessControlList) {827 imagePresignedUrl(imgMd5Base64: $md5, imageSuffix: $suffix, acl: $acl) {828 imageId829 uploadUrl830 uploadHeaders831 }832 }833834 mutation GetVideoUploadUrl($md5: String!, $format: MediaFormat!, $acl: CannedAccessControlList) {835 videoPresignedUrl(videoMd5Base64: $md5, videoFormat: $format, acl: $acl) {836 videoId837 uploadUrl838 uploadHeaders839 }840 }841842 mutation GetFileUploadUrl($md5: String!, $format: MediaFormat!, $name: String, $suffix: String, $sizeBytes: Int, $acl: CannedAccessControlList) {843 filePresignedUrl(844 md5Base64: $md5845 format: $format846 name: $name847 suffix: $suffix848 sizeBytes: $sizeBytes849 acl: $acl850 ) {851 fileId852 uploadHeaders853 uploadUrl854 }855 }856 ```857858 - **`CannedAccessControlList`** (recommended: `PRIVATE`):859 - AUTHENTICATE_READ, AWS_EXEC_READ, BUCKET_OWNER_FULL_CONTROL, BUCKET_OWNER_READ, DEFAULT, LOG_DELIVERY_WRITE, PRIVATE, PUBLIC_READ, PUBLIC_READ_WRITE860 - **`MediaFormat`**:861 - CSS, CSV, DOC, DOCX, GIF, HTML, ICO, JPEG, JPG, JSON, MOV, MP3, MP4, OTHER, PDF, PNG, PPT, PPTX, SVG, TXT, WAV, WEBP, XLS, XLSX, XML862863#### Step 2: Upload the File and Use the Returned ID8641. The mutation response includes:865 - The asset's unique ID (`imageId`, `videoId`, or `fileId`)866 - A presigned `uploadUrl`867 - Any required `uploadHeaders`8682. **Upload the file**:869 - Perform an HTTP `PUT` request to the `uploadUrl` with the raw file data870 - Include any `uploadHeaders` from the mutation response8713. **Reference the asset**:872 - Use the returned ID as the value for the corresponding `*_id` field in your Zion data mutation (e.g., `cover_image_id: returnedImageId`)873874> **Note:** This two-step process is **mandatory** for all media uploads in Zion.app.875876## Third-Party APIs877878A project built on Zion.app can have many third-party HTTP APIs imported. These are separated into two categories: query or mutation, roughly (though not always the case) corresponding to the semantics of HTTP GET vs POST. 879Each API is stored in the following data structure:880```typescript881type ScalarType = 'string' | 'boolean' | 'number' | 'integer';882type TypeDefinition =883 | ScalarType884 | { [key: string]: TypeDefinition | TypeDefinition[] };885886interface ThirdPartyApiConfig {887 id: string;888 name: string;889 operation: 'query' | 'mutation';890 inputs: { [key: string]: TypeDefinition };891 outputs: { [key: string]: TypeDefinition };892}893```894N.B. The value of the operation field within ThirdPartyApiConfig determines the root GraphQL field. i.e. query -> query operation_${id}, and mutation -> mutation operation_${id}.895896### Invocation process897Each input should be provided unless the user asks to remove it. 898e.g. 899Given TPA configuration as follows:900```json901 {902 "id": "lzb3ownk",903 "inputs": {904 "body": {905 "summary": "string",906 "location": "string",907 "description": "string",908 "start": {909 "dateTime": "string",910 "timeZone": "string"911 },912 "end": {913 "dateTime": "string",914 "timeZone": "string"915 },916 "attendees": [917 "string"918 ]919 },920 "Authorization": "string"921 },922 "outputs": {923 "body": {924 "kind": "string",925 "etag": "string",926 "id": "string",927 "status": "string",928 "htmlLink": "string",929 "created": "string",930 "updated": "string",931 "summary": "string",932 "description": "string",933 "location": "string",934 "creator": {935 "email": "string",936 "self": "boolean"937 },938 "organizer": {939 "email": "string",940 "self": "boolean"941 },942 "start": {943944…(truncated)