# ZeyOS guides — LLM full working set

Client guide, iXML guide and SDK. All exports: /ai/guide/index.json

## Choose your task

The `zeyos` skill route "Use ZeyOS business data" is listed under "Use a coding agent with ZeyOS".

### Build a ZeyOS app

Write native iXML and Zymba that runs inside ZeyOS, and package it as an application.

Prerequisites: The ZeyOS SDK (zeysdk) and a linked sandbox instance; Permission to send source to that instance.

Read in this order:
1. Set up the SDK: /ixml/ixml-guide/development-environment/sdk#first-time-setup (Markdown: /ai/guide/ixml/ixml-guide/development-environment/sdk.md)
2. App structure: /ixml/ixml-guide/development-environment/app-structure (Markdown: /ai/guide/ixml/ixml-guide/development-environment/app-structure.md)
3. Execution contexts: /ixml/ixml-guide/development-environment/execution-contexts (Markdown: /ai/guide/ixml/ixml-guide/development-environment/execution-contexts.md)
4. Minimal app: /ixml/ixml-guide/development-environment/minimal-app (Markdown: /ai/guide/ixml/ixml-guide/development-environment/minimal-app.md)
5. Verification checkpoints: /ixml/ixml-guide/development-environment/verification-checkpoints (Markdown: /ai/guide/ixml/ixml-guide/development-environment/verification-checkpoints.md)

First success: You run `zeysdk compile` on the minimal app against your sandbox and it reports no errors. The published sample compiles on a sandbox but has not been run on an instance, so this run is your first evidence for your own instance.
Sample: Minimal app: /ixml/ixml-guide/development-environment/minimal-app
Troubleshooting: Native troubleshooting: /ixml/ixml-guide/development-environment/troubleshooting
Reference: /ixml, /zymba, /db, /sdk

### Connect an external application

Read and write ZeyOS data from a service, browser app or script running outside ZeyOS.

Prerequisites: Access to a ZeyOS instance; OAuth client credentials; Node.js 18 or later for the JavaScript client.

Read in this order:
1. Integration architecture: /guide/tutorials/integration-architecture (Markdown: /ai/guide/guide/tutorials/integration-architecture.md)
2. Authentication: /guide/api-reference/authentication (Markdown: /ai/guide/guide/api-reference/authentication.md)
3. JavaScript client: getting started: /guide/javascript-client/getting-started (Markdown: /ai/guide/guide/javascript-client/getting-started.md)
4. Making requests: /guide/javascript-client/making-requests (Markdown: /ai/guide/guide/javascript-client/making-requests.md)
5. Server-side integrations: /guide/tutorials/server-side-integrations (Markdown: /ai/guide/guide/tutorials/server-side-integrations.md)

First success: `client.schema.validate('listTickets', { fields: ['ID', 'name'] })` passes offline, then the same query returns your tickets.
Sample: Dashboard sample app (read-only): /guide/sample-apps/dashboard
Troubleshooting: Practical guide: /guide/javascript-client/practical-guide
Reference: /api, /auth, /db

### Use a coding agent with ZeyOS

Give a coding agent the authoritative references, project instructions and validation tools.

Prerequisites: A coding agent such as Kilo Code, Pi or OpenCode; The ZeyOS CLI and client, version 1.0.0 or later.

Read in this order:
1. Agent setup: /guide/agent-workflows/agent-setup (Markdown: /ai/guide/guide/agent-workflows/agent-setup.md)
2. Project instructions: /ai/agent-instructions.md
3. Agent quickstart: /guide/agent-workflows/agent-quickstart (Markdown: /ai/guide/guide/agent-workflows/agent-quickstart.md)
4. Native code validation: /ixml/ixml-guide/development-environment/coding-agents (Markdown: /ai/guide/ixml/ixml-guide/development-environment/coding-agents.md)

First success: The agent follows the project instructions and shows a request preview without writing anything.
Sample: Agent recipes: /guide/agent-workflows/agent-recipes
Troubleshooting: CLI coverage and escalation: /guide/agent-workflows/cli-coverage-and-escalation
Reference: /docs/ai, /ixml, /zymba, /api, /db

#### Use ZeyOS business data

Answer questions from your records, and run record operations you explicitly request, with or without an agent.

Prerequisites: The `zeyos` CLI, signed in with `zeyos login`.

Read in this order:
1. Agent quickstart: /guide/agent-workflows/agent-quickstart (Markdown: /ai/guide/guide/agent-workflows/agent-quickstart.md)
2. Agent recipes: /guide/agent-workflows/agent-recipes (Markdown: /ai/guide/guide/agent-workflows/agent-recipes.md)
3. Coding agents: /guide/agent-workflows/coding-agents (Markdown: /ai/guide/guide/agent-workflows/coding-agents.md)

First success: `zeyos count accounts --filter '{"type":1,"visibility":0}'` prints your number of customers.
Sample: Agent recipes: /guide/agent-workflows/agent-recipes
Troubleshooting: CLI coverage and escalation: /guide/agent-workflows/cli-coverage-and-escalation

## AI & Integration Guide
- ZeyOS for Agents and Applications: /ai/guide/guide.md
- Agent Quickstart: /ai/guide/guide/agent-workflows/agent-quickstart.md
- Agent Recipes: /ai/guide/guide/agent-workflows/agent-recipes.md
- Agent Setup: /ai/guide/guide/agent-workflows/agent-setup.md
- CLI Coverage and Escalation: /ai/guide/guide/agent-workflows/cli-coverage-and-escalation.md
- Coding Agents: /ai/guide/guide/agent-workflows/coding-agents.md
- Authentication: /ai/guide/guide/api-reference/authentication.md
- Data Retrieval: /ai/guide/guide/api-reference/data-retrieval.md
- Resources: /ai/guide/guide/api-reference/resources.md
- Schema Reference: /ai/guide/guide/api-reference/schema.md
- Commands Reference: /ai/guide/guide/cli/commands.md
- Configuration: /ai/guide/guide/cli/configuration.md
- CLI Getting Started: /ai/guide/guide/cli/getting-started.md
- MCP Server: /ai/guide/guide/cli/mcp-server.md
- Authentication: /ai/guide/guide/javascript-client/authentication.md
- Getting Started: /ai/guide/guide/javascript-client/getting-started.md
- Making API Requests: /ai/guide/guide/javascript-client/making-requests.md
- Practical Implementation Guide: /ai/guide/guide/javascript-client/practical-guide.md
- Keeping OKF Fresh: /ai/guide/guide/okf/keeping-fresh.md
- Refining with Loops: /ai/guide/guide/okf/loops.md
- OKF Overview: /ai/guide/guide/okf/overview.md
- Producing & Consuming OKF: /ai/guide/guide/okf/producing-and-consuming.md
- CRM Account List: /ai/guide/guide/sample-apps/crm.md
- Dashboard: /ai/guide/guide/sample-apps/dashboard.md
- Kanban Board: /ai/guide/guide/sample-apps/kanban.md
- Application Developers: /ai/guide/guide/tutorials/application-developers.md
- Browser UI Playbook: /ai/guide/guide/tutorials/build-your-own-zeyos-frontend.md
- Integration Architecture: /ai/guide/guide/tutorials/integration-architecture.md
- Server-Side Integrations: /ai/guide/guide/tutorials/server-side-integrations.md

## iXML Guide
- Introduction: /ai/guide/ixml.md
- Data Types: /ai/guide/ixml/ixml-guide/data-types.md
- Arrays: /ai/guide/ixml/ixml-guide/data-types/arrays.md
- Construction: /ai/guide/ixml/ixml-guide/data-types/arrays/construction.md
- Filtering & Sorting: /ai/guide/ixml/ixml-guide/data-types/arrays/filtering-sorting.md
- Inspection: /ai/guide/ixml/ixml-guide/data-types/arrays/inspection.md
- Set Operations: /ai/guide/ixml/ixml-guide/data-types/arrays/set-operations.md
- Slicing & Structuring: /ai/guide/ixml/ixml-guide/data-types/arrays/slicing.md
- Stack & Queue: /ai/guide/ixml/ixml-guide/data-types/arrays/stack-queue.md
- Booleans: /ai/guide/ixml/ixml-guide/data-types/booleans.md
- Null and Undefined: /ai/guide/ixml/ixml-guide/data-types/null-and-undefined.md
- Numbers: /ai/guide/ixml/ixml-guide/data-types/numbers.md
- Strings: /ai/guide/ixml/ixml-guide/data-types/strings.md
- Type Conversion: /ai/guide/ixml/ixml-guide/data-types/type-conversion.md
- Database Operations: /ai/guide/ixml/ixml-guide/database-operations.md
- Connections & Raw SQL: /ai/guide/ixml/ixml-guide/database-operations/connections.md
- Extending the Data Model: /ai/guide/ixml/ixml-guide/database-operations/extending-data-model.md
- Object Operations: /ai/guide/ixml/ixml-guide/database-operations/object-operations.md
- SQL Operations: /ai/guide/ixml/ixml-guide/database-operations/sql-operations.md
- Tags: /ai/guide/ixml/ixml-guide/database-operations/tags.md
- Development Environment: /ai/guide/ixml/ixml-guide/development-environment.md
- App Structure: /ai/guide/ixml/ixml-guide/development-environment/app-structure.md
- Coding Agents and Native Code Validation: /ai/guide/ixml/ixml-guide/development-environment/coding-agents.md
- Development Console: /ai/guide/ixml/ixml-guide/development-environment/dev-console.md
- Execution Contexts: /ai/guide/ixml/ixml-guide/development-environment/execution-contexts.md
- Minimal App: /ai/guide/ixml/ixml-guide/development-environment/minimal-app.md
- SDK Workflow: /ai/guide/ixml/ixml-guide/development-environment/sdk.md
- Troubleshooting: /ai/guide/ixml/ixml-guide/development-environment/troubleshooting.md
- Verification Checkpoints: /ai/guide/ixml/ixml-guide/development-environment/verification-checkpoints.md
- Error Handling & Debugging: /ai/guide/ixml/ixml-guide/error-handling.md
- API Error Patterns: /ai/guide/ixml/ixml-guide/error-handling/api-error-patterns.md
- Best Practices: /ai/guide/ixml/ixml-guide/error-handling/best-practices.md
- Debugging & Troubleshooting: /ai/guide/ixml/ixml-guide/error-handling/debugging.md
- Throwing Errors: /ai/guide/ixml/ixml-guide/error-handling/throwing-errors.md
- Try / Catch: /ai/guide/ixml/ixml-guide/error-handling/try-catch.md
- Arithmetic Expressions: /ai/guide/ixml/ixml-guide/introduction/arithmetic.md
- Quick Reference: /ai/guide/ixml/ixml-guide/introduction/quick-reference.md
- Result Propagation: /ai/guide/ixml/ixml-guide/introduction/result-propagation.md
- Scope and Assignment: /ai/guide/ixml/ixml-guide/introduction/scope-and-assignment.md
- Variables and Substitution: /ai/guide/ixml/ixml-guide/introduction/variables-and-substitution.md
- Reusability and OOP: /ai/guide/ixml/ixml-guide/reusability-and-oop.md
- Classes & Objects: /ai/guide/ixml/ixml-guide/reusability-and-oop/classes.md
- Client Bindings: /ai/guide/ixml/ixml-guide/reusability-and-oop/clients.md
- Functions & Includes: /ai/guide/ixml/ixml-guide/reusability-and-oop/functions.md
- Macros: /ai/guide/ixml/ixml-guide/reusability-and-oop/macros.md
- OOP Patterns: /ai/guide/ixml/ixml-guide/reusability-and-oop/patterns.md
- Standard Library: /ai/guide/ixml/ixml-guide/standard-library.md
- Encoding and Decoding: /ai/guide/ixml/ixml-guide/standard-library/encoding.md
- Excel and ZIP: /ai/guide/ixml/ixml-guide/standard-library/excel-zip.md
- File & Storage Operations: /ai/guide/ixml/ixml-guide/standard-library/filesystem.md
- REST and HTTP Operations: /ai/guide/ixml/ixml-guide/standard-library/http-rest.md
- Mathematics and Date/Time: /ai/guide/ixml/ixml-guide/standard-library/math-datetime.md
- PDF Generation: /ai/guide/ixml/ixml-guide/standard-library/pdf.md
- Syntax Essentials: /ai/guide/ixml/ixml-guide/syntax-essentials.md
- Branching: /ai/guide/ixml/ixml-guide/syntax-essentials/branching.md
- Conditionals: /ai/guide/ixml/ixml-guide/syntax-essentials/conditionals.md
- Loops: /ai/guide/ixml/ixml-guide/syntax-essentials/loops.md
- Output & Serialization: /ai/guide/ixml/ixml-guide/syntax-essentials/output.md
- String Operations: /ai/guide/ixml/ixml-guide/syntax-essentials/strings.md
- Type Introspection: /ai/guide/ixml/ixml-guide/syntax-essentials/type-introspection.md
- Variables: /ai/guide/ixml/ixml-guide/syntax-essentials/variables.md
- User Interface & Forms: /ai/guide/ixml/ixml-guide/user-interface.md
- Exports: /ai/guide/ixml/ixml-guide/user-interface/exports.md
- Forms: /ai/guide/ixml/ixml-guide/user-interface/forms.md
- Weblets: /ai/guide/ixml/ixml-guide/user-interface/weblets.md

## SDK
- ZeyOS SDK Documentation: /ai/guide/sdk.md
- Application Activity Management: /ai/guide/sdk/activity-management.md
- Activate Application: /ai/guide/sdk/activity-management/zeysdk-activate.md
- Deactivate Application: /ai/guide/sdk/activity-management/zeysdk-deactivate.md
- Pre-delete Application: /ai/guide/sdk/activity-management/zeysdk-predelete.md
- Application Management: /ai/guide/sdk/application-management.md
- Clone Application: /ai/guide/sdk/application-management/zeysdk-clone.md
- Configure Application: /ai/guide/sdk/application-management/zeysdk-configure.md
- Create Application: /ai/guide/sdk/application-management/zeysdk-create.md
- Delete Application: /ai/guide/sdk/application-management/zeysdk-delete.md
- Host Commands: /ai/guide/sdk/application-management/zeysdk-host.md
- Migrate Application: /ai/guide/sdk/application-management/zeysdk-migrate.md
- Rollout Application: /ai/guide/sdk/application-management/zeysdk-rollout.md
- Asset Management: /ai/guide/sdk/asset-management.md
- Add Asset: /ai/guide/sdk/asset-management/zeysdk-add.md
- Remove Asset: /ai/guide/sdk/asset-management/zeysdk-remove.md
- Rename Asset: /ai/guide/sdk/asset-management/zeysdk-rename.md
- Update Asset: /ai/guide/sdk/asset-management/zeysdk-update.md
- Authentication: /ai/guide/sdk/authentication.md
- Bundle Management: /ai/guide/sdk/bundle-management.md
- Create Bundle: /ai/guide/sdk/bundle-management/zeysdk-bundle-create.md
- Install Bundle: /ai/guide/sdk/bundle-management/zeysdk-bundle-install.md
- Configuration Files: /ai/guide/sdk/configuration-files.md
- Debugging and Compilation: /ai/guide/sdk/debugging.md
- ZeyOS Entities: /ai/guide/sdk/entities.md
- Platform Management: /ai/guide/sdk/platform-management.md
- Link Instance: /ai/guide/sdk/platform-management/zeysdk-link.md
- Unlink Instance: /ai/guide/sdk/platform-management/zeysdk-unlink.md
- Use Instance: /ai/guide/sdk/platform-management/zeysdk-use.md
- Show Current Instance: /ai/guide/sdk/platform-management/zeysdk-whoami.md
- Snapshot Management: /ai/guide/sdk/snapshot-management.md
- Create Snapshot: /ai/guide/sdk/snapshot-management/zeysdk-snapshot-create.md
- Delete Snapshot: /ai/guide/sdk/snapshot-management/zeysdk-snapshot-delete.md
- List Snapshots: /ai/guide/sdk/snapshot-management/zeysdk-snapshot-list.md
- Restore Snapshot: /ai/guide/sdk/snapshot-management/zeysdk-snapshot-restore.md
- Synchronization: /ai/guide/sdk/synchronization.md
- Compare Files (zeysdk compare): /ai/guide/sdk/synchronization/zeysdk-compare.md
- Pull Files: /ai/guide/sdk/synchronization/zeysdk-pull.md
- Push Files: /ai/guide/sdk/synchronization/zeysdk-push.md
- Recover Sync State: /ai/guide/sdk/synchronization/zeysdk-recover.md
- Sync Files (zeysdk sync): /ai/guide/sdk/synchronization/zeysdk-sync.md
- Watch Files: /ai/guide/sdk/synchronization/zeysdk-watch.md
- ZeyOS Views Reference: /ai/guide/sdk/views.md

---

Canonical: https://www.zeyos.com/guide

# ZeyOS for Agents and Applications

ZeyOS is a business platform that unifies CRM, project management, ticketing, invoicing, and related operational data behind a single API surface. For external integrations, you should think of ZeyOS as the central provider for business data and business logic that feeds coding agents, connected applications, internal tools, and automation services.

Start from what you want to do. The first two paths are also routes a coding agent classifies tasks into. The third covers working through an agent, and contains the third route, **Use ZeyOS business data**.

## Build a ZeyOS app

For native apps that run inside ZeyOS: manifests, services, resources, weblets, hooks and settings, written in iXML or Zymba and managed with `zeysdk`.

**Prerequisites:** the ZeyOS SDK, a linked sandbox instance, and permission to send source to it.

**Recommended reading:**

1. [App structure](https://www.zeyos.com/ixml/ixml-guide/development-environment/app-structure)
2. [Execution contexts](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts)
3. [Agent Setup](https://www.zeyos.com/guide/agent-workflows/agent-setup), if a coding agent is helping
4. [Verification checkpoints](https://www.zeyos.com/ixml/ixml-guide/development-environment/verification-checkpoints)

**First success:** the [minimal app](https://www.zeyos.com/ixml/ixml-guide/development-environment/minimal-app) compiles with `zeysdk compile` and behaves as expected in its real context.

**When stuck:** [native troubleshooting](https://www.zeyos.com/ixml/ixml-guide/development-environment/troubleshooting).

## Connect an external application

For software outside ZeyOS that reads or writes it through the CLI, MCP, `@zeyos/client` or REST.

**Prerequisites:** a ZeyOS instance, OAuth client credentials, and Node.js 18 or later for the JavaScript client.

**Recommended reading:**

1. [Integration Architecture](https://www.zeyos.com/guide/tutorials/integration-architecture)
2. [JavaScript client: Getting Started](https://www.zeyos.com/guide/javascript-client/getting-started)
3. [Making Requests](https://www.zeyos.com/guide/javascript-client/making-requests)
4. [CLI Coverage and Escalation](https://www.zeyos.com/guide/agent-workflows/cli-coverage-and-escalation)

**First success:** `client.schema.validate('listTickets', { fields: ['ID', 'name'] })` passes offline, then the same query returns your tickets.

**Sample:** the read-only [Dashboard](https://www.zeyos.com/guide/sample-apps/dashboard). Move on to the [Kanban board](https://www.zeyos.com/guide/sample-apps/kanban) when you need writes. **When stuck:** the [Practical Guide](https://www.zeyos.com/guide/javascript-client/practical-guide).

## Use a coding agent with ZeyOS

For working through Claude Code, Codex, OpenCode, Pi or another coding agent. The `zeyos` router skill classifies each task as one of the two paths above or as **Use ZeyOS business data** below, and loading a skill authorizes nothing.

**Prerequisites:** a coding agent and the `zeyos` CLI.

**Recommended reading:**

1. [Coding Agents](https://www.zeyos.com/guide/agent-workflows/coding-agents)
2. [Agent Setup](https://www.zeyos.com/guide/agent-workflows/agent-setup)

**First success:** `zeyos skills install zeyos`, then `zeyos skills verify` reports every file verified.

**When stuck:** run `zeyos doctor agent`.

### Use ZeyOS business data

For questions answered from your records, and record operations you explicitly request. You can also do this without an agent.

**Prerequisites:** the `zeyos` CLI, signed in to your instance with `zeyos login`.

**Recommended reading:**

1. [Agent Quickstart](https://www.zeyos.com/guide/agent-workflows/agent-quickstart)
2. [Agent Recipes](https://www.zeyos.com/guide/agent-workflows/agent-recipes)
3. [Coding Agents](https://www.zeyos.com/guide/agent-workflows/coding-agents), for the skills that define metrics such as revenue and workload

**First success:** `zeyos count accounts --filter '{"type":1,"visibility":0}'` prints your number of customers.

**Sample:** the worked examples in [Agent Recipes](https://www.zeyos.com/guide/agent-workflows/agent-recipes). **When stuck:** run `zeyos doctor agent`, then see [CLI Coverage and Escalation](https://www.zeyos.com/guide/agent-workflows/cli-coverage-and-escalation).

:::info Scope
This documentation set primarily covers **external integrations**: the CLI, JavaScript client, REST/OpenAPI, and reusable application patterns. The [coding-agent setup](https://www.zeyos.com/guide/agent-workflows/agent-setup) also connects this workflow to native iXML/Zymba development, with [reusable instructions](https://www.zeyos.com/ai/agent-instructions.md) and links to the language references.
:::

## How Are You Integrating?

| Interface | Best for | Start here |
|-----------|----------|------------|
| **CLI** (`zeyos`) | Coding agents, shell automation, operational CRUD on curated resources | [Coding Agents](https://www.zeyos.com/guide/agent-workflows/coding-agents) |
| **JavaScript Client** (`@zeyos/client`) | Browser apps, Node services, scheduled jobs, full generated API coverage | [Application Developers](https://www.zeyos.com/guide/tutorials/application-developers) |
| **REST/OpenAPI** | Non-JavaScript runtimes, SDK generation, custom HTTP clients | [API Reference](https://www.zeyos.com/guide/api-reference/resources) |

### Coverage Boundary

- The **CLI** is the default interface for coding agents, but it intentionally exposes a curated registry of common resources. Use `zeyos resources` to see the supported set.
- The **JavaScript client** covers the broader generated API surface and is the recommended escalation path when the CLI registry is not enough.
- The **raw API reference** remains the lowest-level source of truth for endpoints, query language, and schema details.

## API Base URL

Every ZeyOS instance exposes its REST API at a predictable base URL:

```
https://cloud.zeyos.com/{INSTANCE}/api/v1/
```

Replace `{INSTANCE}` with your ZeyOS instance identifier (e.g. `demo`, `acme-corp`).

## Authentication

All API requests require authentication. ZeyOS supports two authentication methods:

- **OAuth 2.0 Bearer Tokens** -- The recommended approach for server-side integrations, scheduled jobs, CLI tools, and browser apps that receive tokens from a trusted flow. Obtain tokens through the ZeyOS OAuth2 endpoint at `https://cloud.zeyos.com/{INSTANCE}/oauth2/v1/`.
- **Session Cookies** -- For browser-based applications where the user is already logged into ZeyOS. The browser sends the `ZEYOSID` session cookie automatically.

Include a bearer token in the `Authorization` header for all API requests:

```
Authorization: Bearer YOUR_ACCESS_TOKEN
```

See the [Authentication](https://www.zeyos.com/guide/api-reference/authentication) guide for full details on the OAuth 2.0 Authorization Code flow, token refresh, token revocation, and session-based authentication.

## Return Values and Error Handling

The ZeyOS REST API returns JSON data along with an HTTP status code indicating the outcome of a request.

- **HTTP 200** or **201** indicates a successful response. The result is a **JSON object**.
- **HTTP 400** or greater indicates an error. The response is a **text message** describing the problem.

We recommend treating any HTTP status code greater than or equal to 400 as an error.

## HTTP Methods

The API uses the following HTTP methods, which differ from typical REST conventions in some cases:

| Operation | HTTP Method | Description |
|-----------|-------------|-------------|
| **List** | `POST` | Retrieve a filtered list of records (query parameters are sent in the request body) |
| **Get** | `GET` | Retrieve a single record by ID |
| **Create** | `PUT` | Create a new record |
| **Update** | `PATCH` | Partially update an existing record |
| **Delete** | `DELETE` | Delete a record by ID |
| **Exists** | `HEAD` | Check if a record exists (returns no body) |

:::info
**List** operations use `POST` rather than `GET` because the query language (filters, field selection, sorting) can produce payloads that exceed URL length limits. Sending the query in the request body ensures complex queries work reliably.
:::

## Integration Conventions

Across the CLI, JavaScript client, and sample applications, the same operational rules apply:

- Prefer `filters` in JavaScript client code for compatibility across scalar and foreign-key fields.
- Use `visibility: 0` on resources that **have** the column — `accounts`, `contacts`, `tickets`, `tasks`, `projects`, `items`, `documents`, `notes`, `opportunities`, `appointments`, `campaigns`, `mailinglists`, `storages`, `contracts`, `ledgers` — unless you want archived (`1`) or deleted (`2`) records. **Most other resources have no `visibility` column** — `transactions` (and every billing/procurement entity), `payments`, `messages`, `actionsteps`, `addresses`, `users`, `prices`, `dunning` — and filtering on it there returns an opaque HTTP 400. Check with `zeyos describe <entity>` when unsure.
- For updates, pass changed fields alongside the `ID` directly (`{ ID, status }`) or wrap them in an explicit `body` object (`{ ID, body: { status } }`) — both work; the explicit `body` is only needed to disambiguate a payload field that collides with a reserved control key.
- Treat `extdata` and `expand` as separate features:
  - `extdata` includes custom fields
  - `expand` inlines JSON or binary columns
- Treat count-enabled list responses defensively. Different endpoints or client layers may return either a count wrapper or a list wrapper with count metadata.

## Data Retrieval

The API provides a flexible query language for retrieving data from any resource endpoint. List requests accept a JSON body with the following capabilities:

- **Field selection** -- Request only the columns you need, including fields from related records via dot notation (e.g. `contact.city`, `assigneduser.name`).
- **Filters** -- Restrict results using equality checks, comparison operators (`<`, `>`, `IN`, etc.), string matching (`~`, `~~`, etc.), and composite logic (`AND`, `OR`, `NOT`).
- **Full-text search** -- Search across a resource's indexed text fields with the `query` parameter.
- **Sorting** -- Order results by one or more fields, ascending (`+`) or descending (`-`).
- **Pagination** -- Control result size with `limit` and `offset`. Use `count: true` to retrieve the total number of matching records.
- **Extended data** -- Include custom/extended data fields by passing `extdata: 1` as a request parameter.
- **Expand** -- Use the `expand` parameter to include JSON or binary column data that is omitted from list responses by default (e.g. `binfile` on messages, `items` on transactions).

See the [Data Retrieval](https://www.zeyos.com/guide/api-reference/data-retrieval) guide for the complete query language reference with examples.

## Developer Tools

This project includes tools to help you work with the ZeyOS REST API:

| Tool | Description | Docs |
|------|-------------|------|
| **JavaScript Client** (`@zeyos/client`) | A dependency-light, auto-generated client library with generated methods for every API operation, OAuth 2.0 helpers, and automatic token refresh. Works in browsers and Node.js 18+. | [Getting Started](https://www.zeyos.com/guide/javascript-client/getting-started) |
| **CLI** | A command-line interface for scripting and automation against your ZeyOS instance. Supports filtering, sorting, pagination, and multiple output formats. | [Getting Started](https://www.zeyos.com/guide/cli/getting-started) |
| **Agent Workflows** | CLI-first guidance for coding agents, JSON-first recipes, and escalation rules when the CLI registry is not enough. | [Coding Agents](https://www.zeyos.com/guide/agent-workflows/coding-agents) |
| **Application Developers** | Architecture, browser UI patterns, and server-side integration guidance for connected applications. | [Application Developers](https://www.zeyos.com/guide/tutorials/application-developers) |
| **Practical Guide** | A field-tested collection of implementation patterns, gotchas, and best practices learned from building real applications against the ZeyOS API. | [Practical Guide](https://www.zeyos.com/guide/javascript-client/practical-guide) |
| **Schema Reference** | Field names, types, enum values, and GIN-indexed fields for the 20 most commonly used ZeyOS resources. | [Schema Reference](https://www.zeyos.com/guide/api-reference/schema) |

## Sample Applications

Three ready-to-run sample applications demonstrate different integration patterns:

| Sample | What it demonstrates | Docs |
|--------|---------------------|------|
| **Kanban Board** | Drag-and-drop ticket management, optimistic UI updates, status changes via PATCH, task tables, project/account filtering | [Kanban](https://www.zeyos.com/guide/sample-apps/kanban) |
| **CRM Account List** | Dot-notation joins, field aliasing, full-text search, sortable columns, pagination, modal editing | [CRM](https://www.zeyos.com/guide/sample-apps/crm) |
| **Dashboard** | KPI cards with `count: true`, parallel API queries, status distribution charts, overdue calculations, read-only patterns | [Dashboard](https://www.zeyos.com/guide/sample-apps/dashboard) |

## Prerequisites

- **A ZeyOS instance** -- You need access to a ZeyOS cloud instance. Your instance URL follows the pattern `https://cloud.zeyos.com/{INSTANCE}/`.
- **Node.js 18+** -- Required for the JavaScript client.
- **Node.js 18.3+** -- Required for the CLI package.

## Quick Example

List the 10 most recently modified open tickets using `curl`:

```bash
curl -X POST "https://cloud.zeyos.com/demo/api/v1/tickets" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "fields": ["ID", "name", "status", "priority", "duedate"],
    "filter": {"visibility": 0},
    "sort": ["-lastmodified"],
    "limit": 10
  }'
```

The same request using the JavaScript client:

```js
import { createZeyosClient, MemoryTokenStore } from '@zeyos/client';

const client = createZeyosClient({
  platform: 'https://cloud.zeyos.com/demo/',
  auth: {
    mode: 'oauth',
    oauth: {
      tokenStore: new MemoryTokenStore({
        accessToken: 'YOUR_ACCESS_TOKEN',
      }),
    },
  },
});

const tickets = await client.api.listTickets({
  fields: ['ID', 'name', 'status', 'priority', 'duedate'],
  filters: { visibility: 0 },
  sort: ['-lastmodified'],
  limit: 10,
});

console.log(tickets);
```

For long-lived OAuth sessions in trusted code, add `clientId`, `clientSecret`, and a refresh-capable token store. For browser-only apps, prefer session mode or a backend token broker instead of shipping client credentials.

:::tip
You can also use **session authentication** when running inside a browser where the user is already logged into ZeyOS. Set `auth.mode` to `'session'` and the client will use browser cookies automatically. See the [Authentication](https://www.zeyos.com/guide/api-reference/authentication) guide for details.
:::

## Next Steps

- **[Coding Agents](https://www.zeyos.com/guide/agent-workflows/coding-agents)** -- CLI-first workflows, JSON output patterns, and escalation guidance
- **[Application Developers](https://www.zeyos.com/guide/tutorials/application-developers)** -- browser and server-side integration paths
- **[Getting Started](https://www.zeyos.com/guide/javascript-client/getting-started)** -- Install the JavaScript client and make your first API call
- **[Making Requests](https://www.zeyos.com/guide/javascript-client/making-requests)** -- CRUD operations, filtering, sorting, pagination, and error handling
- **[Practical Guide](https://www.zeyos.com/guide/javascript-client/practical-guide)** -- Patterns and gotchas from real-world implementations
- **[Schema Reference](https://www.zeyos.com/guide/api-reference/schema)** -- Field definitions and enum values for all major resources
- **[Browser UI Playbook](https://www.zeyos.com/guide/tutorials/build-your-own-zeyos-frontend)** -- Step-by-step browser integration guide

---

Canonical: https://www.zeyos.com/guide/agent-workflows/agent-quickstart

# Agent Quickstart

This guide gets a coding agent from zero to authenticated CRUD access with the ZeyOS CLI.

## Install or Run the CLI

From this repository:

```bash
npm install
node cli/bin/zeyos.mjs --help
```

For a global `zeyos` command during development:

```bash
npm link cli/
zeyos --help
```

## Load the ZeyOS Skills

Before doing real work, install the bundled skill packs so the agent picks up ZeyOS's query conventions (entity model, `filters` usage, safe writes):

```bash
zeyos skills list                 # see what's available
zeyos skills install zeyos        # interactive: pick an agent (claude, codex, opencode, droid, pi…), then local/global
zeyos skills install zeyos --target claude --global -y   # or non-interactively with flags
zeyos skills verify --target claude --global             # check the installed files
```

This is the recommended entry point for an agent: the `zeyos` router classifies the task, and the domain skills encode how to resolve names to IDs, which resource to query first, and how to escalate from the CLI to `@zeyos/client`.

## Authenticate

Interactive login:

```bash
zeyos login
```

Pre-fill login parameters:

```bash
zeyos login \
  --base-url https://cloud.zeyos.com/demo \
  --client-id myapp \
  --secret "$ZEYOS_CLIENT_SECRET"
```

This still starts the OAuth authorization-code flow and requires a browser redirect or pasted code. For fully unattended agents, inject `ZEYOS_BASE_URL`, `ZEYOS_TOKEN`, and optionally `ZEYOS_REFRESH_TOKEN`, `ZEYOS_CLIENT_ID`, and `ZEYOS_CLIENT_SECRET` through the environment.

The CLI stores credentials in `.zeyos/auth.json` in the current project tree, or in `~/.config/zeyos/credentials.json` when `--global` is used.

## Verify the Session

Check the current user:

```bash
zeyos whoami --json
```

`whoami` hides access tokens by default. Use `zeyos whoami --show-token --json` only when the token must be handed to another local tool.

Discover which curated resources the CLI can operate on directly:

```bash
zeyos resources
```

Inspect a resource's fields, types and enum values before querying (works offline, no login needed):

```bash
zeyos describe tickets
```

## Read Data

List tickets in machine-readable form:

```bash
zeyos list tickets --json
```

List only the fields an agent needs:

```bash
zeyos list tickets \
  --fields ID,ticketnum,name,status,priority,lastmodified \
  --filter '{"visibility":0}' \
  --sort -lastmodified \
  --limit 20 \
  --json
```

Fetch one record:

```bash
zeyos get ticket 42 --all --json
```

Count matching records:

```bash
zeyos count tickets --filter '{"visibility":0,"status":4}' --json
zeyos count customfields --json
zeyos count actionsteps --filter '{"status":0}' --json
```

Summarize actionstep effort/time-entry evidence:

```bash
zeyos list actionsteps \
  --fields ID,name,status,date,duedate,effort,ticket,task,account \
  --filter '{"status":3}' \
  --limit 100 \
  --json
```

Inspect ticket-linked mail without sending anything:

```bash
zeyos list messages \
  --fields ID,date,mailbox,subject,sender_email,to_email,ticket,reference,messageid \
  --filter '{"ticket":42}' \
  --sort +date \
  --json
```

## Write Data

Create with JSON-first input:

```bash
zeyos create ticket \
  --data '{"name":"Follow up with customer","status":0,"priority":2,"visibility":0}' \
  --json
```

Update with JSON-first input:

```bash
zeyos update ticket 42 \
  --data '{"status":4,"priority":3}' \
  --json
```

Delete interactively:

```bash
zeyos delete ticket 42
```

Delete non-interactively only when the automation has already decided the record must be removed:

```bash
zeyos delete ticket 42 --force
```

## Safe Defaults for Agents

- Use `--json` unless a human is actively reading the output.
- Use `visibility: 0` on resources that **have** the column — `accounts`, `contacts`, `tickets`, `tasks`, `projects`, `items`, `documents`, `notes`, `opportunities`, `appointments`, `campaigns`, `mailinglists`, `storages`, `contracts`, `ledgers` — unless you want archived (`1`) or deleted (`2`) records. **Most other resources have no `visibility` column** — `transactions` (and every billing/procurement entity), `payments`, `messages`, `actionsteps`, `addresses`, `users`, `prices`, `dunning` — and filtering on it there returns an opaque HTTP 400. Check with `zeyos describe <entity>` when unsure.
- Prefer `--data '<json>'` over many separate flags in automation.
- Run `zeyos resources` before assuming a resource is CLI-supported.
- Use `actionsteps.effort` for time-entry totals; do not infer booked time from task assignment.
- Draft e-mail text in the response unless the user explicitly asks for a real ZeyOS draft record; never send mail from an agent workflow.
- Escalate to [`@zeyos/client`](https://www.zeyos.com/guide/agent-workflows/cli-coverage-and-escalation) when the CLI resource registry is not enough.

---

Canonical: https://www.zeyos.com/guide/agent-workflows/agent-recipes

# Agent Recipes

These workflows are designed for coding agents and shell automation. Every example is safe to convert into CI jobs, local scripts, or ad hoc operational tooling.

## Work a Ticket Queue

List active tickets with only the fields you need:

```bash
zeyos list tickets \
  --fields ID,ticketnum,name,status,priority,assigneduser,lastmodified \
  --filter '{"visibility":0,"status":4}' \
  --sort -lastmodified \
  --limit 50 \
  --json
```

Count the backlog before acting:

```bash
zeyos count tickets --filter '{"visibility":0,"status":0}' --json
```

Move a ticket to a new status:

```bash
zeyos update ticket 42 --data '{"status":7}' --json
```

## Create a Follow-up Ticket

```bash
zeyos create ticket \
  --data '{"name":"Escalate billing issue","status":0,"priority":3,"account":15,"visibility":0}' \
  --json
```

Use `zeyos get ticket <id> --json` immediately after creation if the workflow needs server-confirmed fields or related data.

## Query Accounts for an Agent-Facing CRM View

Use field aliases and joins when you need a compact response:

```bash
zeyos list accounts \
  --fields '{"Id":"ID","Name":"lastname","City":"contact.city","Agent":"assigneduser.name"}' \
  --filter '{"visibility":0}' \
  --sort +lastname \
  --limit 25 \
  --json
```

## Pull Tasks for a Project or Ticket

```bash
zeyos list tasks \
  --fields ID,tasknum,name,status,priority,duedate \
  --filter '{"visibility":0,"project":123}' \
  --sort +duedate \
  --json
```

```bash
zeyos list tasks \
  --fields ID,tasknum,name,status,priority,duedate \
  --filter '{"visibility":0,"ticket":42}' \
  --json
```

## Paginate Deterministically

```bash
zeyos list tickets \
  --filter '{"visibility":0}' \
  --sort -lastmodified \
  --limit 100 \
  --offset 0 \
  --json
```

Advance by incrementing `--offset` yourself. For long-running jobs, always keep the sort explicit so the ordering stays stable.

## Pipe to `jq`

Extract IDs:

```bash
zeyos list tickets --fields ID,name --filter '{"visibility":0}' --json | jq '.[].ID'
```

Extract the current access token for another tool:

```bash
zeyos whoami --show-token --json | jq -r '.accessToken'
```

Treat printed access tokens as secrets. Avoid writing them to logs, shared shell history, or agent transcripts.

## Destructive Guardrails

- `zeyos delete` prompts by default. Keep that behavior in human-in-the-loop sessions.
- Use `--force` only in automation that already has a clear selection rule.
- Prefer `count` or a dry-run `list` before a bulk action.
- If a workflow needs unsupported resources, raw request control, or custom retry logic, move it to [`@zeyos/client`](https://www.zeyos.com/guide/agent-workflows/cli-coverage-and-escalation).

---

Canonical: https://www.zeyos.com/guide/agent-workflows/agent-setup

# Agent Setup

Kilo Code, Pi, OpenCode, and other coding agents need the same ZeyOS project context: authoritative references, project instructions, validation tools, and explicit permission to execute or change anything remotely. Configure these once for the project, then adapt how your chosen harness loads them. A capable model can still invent an iXML attribute or database column if it works from memory.

Each task belongs to one route, and the `zeyos` router skill classifies it first:

| Route | Use it for | Interfaces |
| --- | --- | --- |
| **Build a ZeyOS app** | Native manifests, services, resources, weblets, hooks and settings | iXML/Zymba, `zeysdk` |
| **Connect an external application** | Software outside ZeyOS that reads or writes it | CLI, MCP, `@zeyos/client`, REST |
| **Use ZeyOS business data** | Questions and explicitly requested record operations | The 15 domain skills, through the CLI or MCP |

An explanation, code edit, review or validation-only task must never trigger a live query, compile, push or run. Loading a skill is not authorization to execute anything.

The tools serve different purposes:

| Work | Tool | Contract to check |
| --- | --- | --- |
| Native iXML/Zymba compilation checks | ZeyOS SDK `zeysdk compile` / `zeysdk lint` | Linked instance compiler; supplied source is not executed |
| Native runtime behavior | ZeyOS SDK and the platform runtime, when execution is requested | Application entry point, inputs, outputs, and side effects |
| Business data queries and bounded updates | `zeyos` CLI or its MCP server | CLI resource registry, database metadata, and API contract |
| Connected JavaScript applications | `@zeyos/client` | Generated operations and runtime schema |

The client validator and MCP server do not compile iXML or Zymba. Use the SDK's `compile` or `lint` for native compilation checks without executing the supplied source. These are remote checks, not offline linters. `zeysdk run` executes code and is not a substitute for validation.

## Set up the project first

1. Open the application repository as the agent's working directory. Identify the native application root or external integration entry point.
2. Download the [ZeyOS project instructions as Markdown](https://www.zeyos.com/ai/agent-instructions.md). Merge them into an existing `AGENTS.md`, or keep the file as `instructions/zeyos.md` and load it using your harness's supported mechanism. Do not overwrite existing project rules.
3. Add a short project map: reference directory, application root, relevant examples, installed tool versions, validation commands, and target profile. Leave unknown values explicitly unset; keep credentials out of the map.
4. Configure a provider and model in the harness. Keep personal authentication in its private credential store and project instructions in version control.
5. Enable the file access and local validation tools needed for the task. Treat remote execution, database writes, synchronization, and deployment as separate capabilities. A request to validate code does not authorize running it.
6. Start a fresh session and ask the agent to identify the loaded instructions, exact reference files, and available non-executing checks before editing.

Prefer a reviewed local copy of the instructions over fetching a mutable remote URL at every session start. Update that copy deliberately when the ZeyOS guidance changes. Keep shared rules in one place and project paths in the project; avoid duplicate or contradictory instruction sources.

### Install and verify the skills

Install the router for your harness, then check the result:

```bash
zeyos skills install zeyos --target opencode --local
zeyos skills verify --target opencode --local
```

Use `--target claude`, `codex`, `droid`, `pi` or `agents` for the other harnesses, or `--dir <path>` for any directory. `install zeyos` also installs `zeyos-dev`, the 15 domain skills, and `shared/`, which holds the shared references, the OKF bundle and a copy of the development contract. The harness only advertises the skills; their content loads on demand. The tree needs no source checkout, and every link in it resolves.

To upgrade, install again after updating the CLI. `--preview` shows the plan and writes nothing. Files you changed are kept unless you pass `--force`. See [Commands → skills](https://www.zeyos.com/guide/cli/commands#skills).

### Connect your coding harness

These are adapters for the same project setup, not separate ZeyOS workflows. Configuration details were checked against the official documentation on **30 October 2026**; check the documentation for your installed version before copying a configuration.

| Harness | Project instructions | Harness configuration |
| --- | --- | --- |
| Kilo Code | Root `AGENTS.md`, or `instructions` paths in `kilo.jsonc` | Configure provider/model and tool permissions in Kilo's settings or the existing project config. Start a new task after changing instructions. |
| Pi | Root `AGENTS.md`; Pi also loads applicable parent-directory context files | Project settings live in `.pi/settings.json`. Choose the model with `/model`; use `/reload` after changing instructions or configuration. |
| OpenCode | Root `AGENTS.md`, or `instructions` paths in `opencode.json` / `opencode.jsonc` | Configure provider/model and permissions in the existing project config. Start from the application directory. For V2, see the [adapter](#opencode-v2-adapter). |

Versions matter for OpenCode: the unversioned `/docs/` pages linked here describe V1, and V2 has its own [agents](https://opencode.ai/v2/docs/agents/), [skills](https://opencode.ai/v2/docs/skills/) and [commands](https://opencode.ai/v2/docs/commands/) pages.

Sources: [Kilo project instructions](https://kilo.ai/docs/customize/agents-md), [Kilo settings](https://kilo.ai/docs/getting-started/settings), [Pi configuration](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/configuration.md), [Pi usage](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/usage.md), [OpenCode rules](https://opencode.ai/docs/rules/), and [OpenCode configuration](https://opencode.ai/docs/config/).

For **Kilo or OpenCode**, merge this into the appropriate config if you keep the downloaded instructions separate:

```json
{
  "instructions": ["instructions/zeyos.md"]
}
```

For **Pi**, merge the instructions into the project's `AGENTS.md` and confirm that the startup header lists it. Do not replace Pi's `SYSTEM.md` merely to add ZeyOS project rules: that file replaces the default system prompt. Pi does not ask before every tool call; use the available tool controls and a sandbox appropriate to the task. Prompt instructions are not access controls. See [Pi configuration](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/configuration.md) and [Pi usage](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/usage.md).

The CLI is enough to begin. Add MCP only if it improves your harness's tool discovery; no retrieval framework or vector database is required for the initial setup.

### OpenCode V2 adapter

This adapter adds a `zeyos-expert` agent plus `/zeyos` and `/zeyos-dev` commands. It references skills by name, so it works from any install location that OpenCode discovers. Install the skills as above, then merge this into your project's `opencode.json`:

```json
{
  "agents": {
    "zeyos-expert": {
      "description": "Works on ZeyOS tasks after classifying them with the zeyos skill.",
      "mode": "primary",
      "system": "You are a ZeyOS engineer. Load the zeyos skill first and follow its routing. Loading a skill authorizes nothing."
    }
  },
  "commands": {
    "zeyos": {
      "description": "Route a ZeyOS task and continue with only the relevant context.",
      "agent": "zeyos-expert",
      "subagent": false,
      "template": "Load the zeyos skill, classify the task and continue with only the relevant context. Task: $ARGUMENTS"
    }
  }
}
```

This excerpt shows the shape. The [complete fragment](https://github.com/zeyos/client/tree/main/agents/adapters/opencode-v2) also defines `/zeyos-dev` and the permission rules that make OpenCode ask before remote compiles, SDK runs, pushes and CLI record writes.

The complete fragment was checked against OpenCode **2.2.5** on 5 November 2026, in an isolated home directory: it validates against the published [config schema](https://opencode.ai/config.json), and OpenCode loaded the agent, discovered all 17 skills and registered both commands. No model session was run, so this does not establish how a given model follows the skills.

## Check the ZeyOS tools and versions

The join and custom-field validation below requires client/CLI **1.0.0 or later**. Check `zeyos --version` and the package version used by your application; upgrade client and CLI together. The [1.0.0 release](https://github.com/zeyos/client/releases/tag/v1.0.0) contains these checks. Native iXML/Zymba tooling comes from the separate `zeysdk` installation. Inspect `zeysdk compile --help` and `zeysdk lint --help`; use the [SDK debugging reference](https://www.zeyos.com/sdk/debugging/) for the supported commands and their effects.

## Retrieve authoritative Zymba entries

Start with the [symbol index](https://www.zeyos.com/ai/zymba/symbols.json) and [namespace index](https://www.zeyos.com/ai/zymba/namespaces.json). For example, find `Array.joinValues` with `kind: "method"`, then inspect its exact `definition` and source JSON pointer. Keep static and instance methods distinct. Fetch a single [Array namespace](https://www.zeyos.com/ai/zymba/namespace.Array.json), or use the [full reference working set](https://www.zeyos.com/zymba/llms-full.txt) when needed.

Each entry retains the source URL, hash, and pointer into the [official JSON mirror](https://www.zeyos.com/__doc/zymbaref.json). Preserve duplicate declarations and investigate differences rather than choosing one silently. Read argument order, types, defaults, returns and annotations from the raw definition; the generated signature is a display aid. An omitted metadata property is not proof that an argument is optional or required. These indexes support reference review, not compilation or runtime certification, and do not promote the deferred Zymba guide drafts.

## Choose a model by testing ZeyOS tasks

For complex ZeyOS work, prioritize correctness over the lowest token price. Start with a provider's capable reasoning/coding model, use its supported reasoning controls, and hold the model fixed while evaluating it. The harness and model are separate choices: changing from Kilo to Pi or OpenCode does not remove the need for the same references and checks.

Select the exact model through the provider's picker and verify its availability in your harness. A model recommendation is a starting point, not a ZeyOS benchmark result. See [Kilo model selection](https://kilo.ai/docs/code-with-ai/agents/model-selection), [Pi model selection](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/models.md), and [OpenCode models](https://opencode.ai/docs/models/).

Evaluate candidates with the same references, tools, prompts, and revision. Run each task more than once. In a client checkout, the [evaluation suite](https://github.com/zeyos/client/blob/main/test/agent-protocol/README.md) turns this kind of acceptance task into scored scenarios: `node test/agent-protocol/harness/run.mjs --layer n`.

| Acceptance task | Required behavior |
| --- | --- |
| Repair `<if condition="...">` | Find the documented comparison attributes; preserve XML escaping |
| Return rows from `db:select` | Distinguish `var` from `var_result` and use the intended result type |
| Join Zymba array values | Find `@Array.joinValues` and its signature; avoid `@String.joinValues` |
| Prepare an account creation | Reject `name`, detect missing `currency`, and preview without creating |
| Query transaction records | Reject an invented `visibility` field; resolve the correct resource |
| Query a custom field or unknown join | Retrieve its definition or report the gap instead of guessing |
| Test a hook or service | Identify its invocation context; do not equate an inline run with a full integration test |
| Recover from a write timeout | Reconcile the outcome before considering a retry |

Require zero invented identifiers in the accepted patch, detection of every deliberately planted error, and no unauthorized side effects. Compare first-pass correctness, repair attempts, total cost, and elapsed time. Only move routine work to a cheaper model after it meets the same criteria. Large context windows and general coding leaderboards do not establish ZeyOS competence.

## Supply focused reference context

In a checkout of the developer guide, use `ref/ixmldoc.xml`, `ref/zymbaref.json`, `ref/zymbadoc.md`, `ref/dbref.json`, `ref/simpledb.sql`, `ref/views.txt`, and the relevant `ref/openapi/` and `ref/sdk/` entries. `LEARN.md` records dated successes and failures; `docs/zymba/` remains draft material. Existing application code supplies conventions, not authority for undocumented behavior.

Without a local checkout, start with [AI Resources](https://www.zeyos.com/docs/ai) and [llms.txt](https://www.zeyos.com/llms.txt). Use the namespace, table, and operation indexes to retrieve only the entries needed. The [artifact manifest](https://www.zeyos.com/ai/manifest.json) provides hashes for caching. A hash identifies a snapshot; it does not establish compatibility with your instance.

Keep a small evidence list for each change: symbol or field, reference location, snapshot/version, and relevant runtime result. Re-read the exact entries after context compaction or a schema update. If a source is inaccessible, surface the gap before implementing dependent behavior.

## Validate native code without executing it

Choose the check by what it establishes and where it runs:

| Check | Executes the submitted application code? | What it establishes |
| --- | --- | --- |
| Local reference and source review | No | Documented commands, arguments, schema names, and application input contracts |
| A local XML parser, for a compatible complete XML document | No | XML well-formedness only; not the iXML language contract |
| `zeysdk compile` | No | Whether selected iXML/Zymba source compiles on the linked ZeyOS instance |
| `zeysdk lint` | No | Compilation results for the eligible local project scripts it selects |
| `zeysdk run` or a real form/hook/service invocation | Yes | Runtime behavior; use only when execution is part of the requested test |

### Review structure and contracts locally

Formatting must preserve text and result propagation. For new iXML snippets use two-space indentation, quoted attributes, and compact text-only elements; preserve established conventions in existing files.

```xml
<set var="name">iXML</set>
<output>Hello $name</output>
```

Check each iXML command's attributes, defaults, child elements, content type, and result bindings against the reference. For calls and includes, inspect the called code and its input contract. An attribute without a documented default is not automatically mandatory. A generic XML parser can reject native fragments, prefixed commands or entities that the ZeyOS parser accepts; do not add invented namespace declarations to make a generic parser pass.

For Zymba, review the receiver, method, argument order, defaults, return value, and mutation behavior against the authoritative definitions. PHP or JavaScript syntax validation does not establish Zymba correctness. Neither a reference index nor an agent's review is a full language validator.

### Ask the ZeyOS compiler for diagnostics

In an already linked application directory, after confirming the intended compiler target and permission to send the source there:

```bash
# Check one selected asset without executing its source.
zeysdk compile services/example.ixml

# Check inline iXML or Zymba without executing it.
zeysdk compile --ixml '<set var="name">iXML</set><output>Hello $name</output>'
zeysdk compile --zy 'return 1;'

# Check eligible modified project scripts, or all eligible local scripts.
zeysdk lint
zeysdk lint --all
```

Replace the example path with the asset you intend to check. `compile` and `lint` submit source to the linked instance's authenticated `PUT sdk/v1/compile` endpoint. They do not execute that source and do not run a sync/push, but they require network access, existing authentication, and application linkage. Treat source transmission separately from a local-only check. `lint --all` still compiles remotely; it is not an offline mode. [SDK compile and lint documentation](https://www.zeyos.com/sdk/debugging/).

Inspect the diagnostics and the selected file count. `lint` selects eligible manifest-backed scripts; it is not a recursive validation of every file in the repository. A successful exit with no eligible scripts is not evidence that changed code was checked. Use `compile` on a specific asset when you need explicit coverage.

A compilation pass does not prove database identifiers, required business inputs, permissions, or behavior in a form/hook/HTTP context. Review those contracts separately. If the task is only to validate code, stop with the diagnostics and remaining coverage gaps. Do not run it merely to get stronger evidence. When execution is explicitly needed, perform a separate, bounded sandbox test through the correct entry point; `zeysdk run` belongs to that stage.

See the [native formatting and validation workflow](https://www.zeyos.com/ixml/ixml-guide/development-environment/coding-agents), [iXML reference](https://www.zeyos.com/ixml/), and [Zymba reference](https://www.zeyos.com/zymba/).

## Prevent invented tables and fields

Resolve names before generating a request. For a join, trace every relationship to its target table and check the terminal field. Do this for selections, filters, sorting, grouping, and updates. Keep SQL tables/views separate from API resources, CLI aliases, and custom fields.

The database reference confirms that accounts have `lastname` and `firstname`, not `name`; `accounts.contact` references `contacts.ID`; `accounts.currency` has no database default; and `transactions` has no `visibility` field. A CLI resource such as a billing-invoice alias is not proof of a SQL table with the same name.

With the existing CLI, start locally:

```bash
zeyos resources --json
zeyos describe accounts --json
zeyos list accounts --fields ID,lastname --limit 5 --dry-run --json
zeyos create account --lastname "Agent sandbox example" --currency EUR --dry-run --json
```

The final command previews a create request; it does not create an account. The current CLI still requires configured authentication to construct these request previews, although the preview does not dispatch the request. Use the credential-free JavaScript schema check below if no profile is configured. Do not remove `--dry-run` until the target and write are authorized. Keep validation enabled. The human CLI flag is `--filter`, while JavaScript and MCP request payloads use `filters`.

For JavaScript, inspect a request without sending it:

```js
import { createZeyosClient } from '@zeyos/client';

const client = createZeyosClient({
  platform: 'https://cloud.zeyos.com/demo/',
  auth: { mode: 'none' },
  validate: true,
});

console.log(client.schema.validate('createAccount', {
  lastname: 'Agent sandbox example',
}));
// valid: false; missing currency. This does not make an API request.

console.log(client.schema.validate('listTransactions', {
  fields: ['ID'],
  filters: { visibility: 0 },
  limit: 5,
}));
// valid: false; transactions has no visibility field.
```

For real requests, use the established authenticated client configuration and retain `validate: true`. The unauthenticated example above is for local schema checks only.

:::warning Validation coverage
Client/CLI 1.0.0 and later check every documented foreign-key hop and terminal field in selections, filters, and sorting. It validates individual custom keys against an explicit `customFields` contract and checks supplied JSON types/enums. Missing custom metadata is an error. Upgrade client and CLI together; older releases accepted these paths unchecked.

It is still not a complete type checker or OpenAPI validator. Required parameters beyond the curated account-currency rule, permissions, record existence, join cardinality, and business rules need separate checks. Relationships absent from the bundled FK metadata are rejected, even if a descriptive column name looks plausible. `describe` reads bundled schema plus your supplied snapshot, not live tenant introspection.
:::

For native database code, apply the same name checks to the database reference and use the documented iXML or Zymba query APIs. The JavaScript validator cannot validate arbitrary SQL. Use documented structured conditions or preparation for values, and allowlist dynamic identifiers. Check cardinality, nullability, and totals as well as syntax: a valid join can still double a sum.

### Configure custom fields once per instance

Use the [snapshot format and setup](https://www.zeyos.com/guide/cli/configuration#custom-field-validation) for CLI/MCP, or the [JavaScript contract](https://www.zeyos.com/guide/javascript-client/making-requests#custom-field-contracts). Set `ZEYOS_CUSTOM_FIELDS_FILE` to its absolute path in the agent environment. Check `customFieldSource.platform` in `describe` output; actual dispatch rejects snapshots from a different instance.

Discover active extdata definitions using `customfields.context` for the owning resource, `source: 0`, and `activity: 0`; use `reference` as the exact key. Page through all results and verify permissions and completeness before declaring an allowlist. `customfields.entity` describes the target of an entity-valued field, not its owner. Display types are not JSON types: add type and enum constraints only from reviewed application contracts. Refresh after configuration changes. Keep snapshots private and credential-free.

Do not repair `custom_fields_unavailable` or `unknown_custom_field` by disabling validation. Retrieve and review the missing definitions. An absent resource map means unknown; an explicit empty map means known empty. Related custom fields use the destination resource's map, such as `contact.extdata.preferred` on accounts using `contacts` definitions.

## Optional MCP connection

The existing [ZeyOS MCP server](https://www.zeyos.com/guide/cli/mcp-server) exposes resource discovery, schema descriptions, and record queries. Install the matching client/CLI version first and ensure the harness can find `zeyos-mcp`. Use your actual configured profile name; `sandbox` below is an example, not evidence of a safe target. Resolve conflicting inherited credential variables and keep authentication in the existing private configuration.

**Kilo and OpenCode** use this structure in their respective project configuration (`kilo.jsonc` or `opencode.json` / `opencode.jsonc`):

```json
{
  "mcp": {
    "zeyos": {
      "type": "local",
      "command": ["zeyos-mcp"],
      "environment": {
        "ZEYOS_PROFILE": "sandbox",
        "ZEYOS_MCP_ALLOW_WRITES": "0"
      },
      "enabled": true
    }
  }
}
```

OpenCode V2 nests servers under `servers`, for example `{ "mcp": { "servers": { "zeyos": { "type": "local", "command": ["zeyos-mcp"], "environment": { "ZEYOS_MCP_ALLOW_WRITES": "0" } } } } }`.

Sources: [Kilo MCP](https://kilo.ai/docs/automate/mcp/using-in-kilo-code) and [OpenCode MCP](https://opencode.ai/docs/mcp-servers/) (V1).

**Pi's current MCP support** uses `.pi/mcp.json` for a trusted project. Its keys differ from the Kilo/OpenCode format:

```json
{
  "mcpServers": {
    "zeyos": {
      "command": "zeyos-mcp",
      "args": [],
      "env": {
        "ZEYOS_PROFILE": "sandbox",
        "ZEYOS_MCP_ALLOW_WRITES": "0"
      }
    }
  }
}
```

Run `pi mcp list` to inspect connections, or use `/mcp` in a session. Use `/reload` after changing the file. Older Pi versions and extensions may use a different integration; use the CLI directly if your installed version does not support this format. See [Pi MCP configuration](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/mcp.md).

Start with `list_resource_types` and `describe_resource`; these work without authenticated API access. MCP write tools require `ZEYOS_MCP_ALLOW_WRITES=1`; there is no delete tool. This switch does not restrict a separate shell or HTTP tool, so configure those permissions and credential scope as well.

The [agent skills](#install-and-verify-the-skills) add routing and task-specific workflows. Project instructions and the CLI are sufficient for the initial setup; a skill installer target for every harness is not required.

## Make automation repeatable

Use a progression with measurable acceptance gates:

1. **Local assistance:** reference lookup, small patches, schema preflight, and review. Record the model and reference versions.
2. **Sandbox execution:** exercise normal, empty, malformed, and missing-input cases. Include real HTTP/hook/form context where relevant. Record expected and actual results without private data.
3. **Bounded operational jobs:** define the instance, resources, allowed fields, record cap, time budget, retry policy, and stop conditions. Begin with a preview; verify each authorized write by reading its result.
4. **Repeatable unattended operation:** prevent overlapping runs, reconcile uncertain outcomes, and retain redacted audit evidence. Use a documented idempotency mechanism where available; otherwise design explicit deduplication and test it. Do not invent an idempotency header or retry creates blindly.

The join validator and explicit custom-field snapshots now cover API identifier checks. The authoritative Zymba indexes support lookup, and the SDK supports non-executing compilation checks. Broader required-parameter/schema validation and a regression set built from recurring failures remain separate work. A checker should distinguish **error**, **verified within its coverage**, and **requires runtime evidence**; it must never turn missing metadata into a silent pass.

Keep rules short and load domain details on demand. Promote only reproducible fixes into shared instructions. Keep the guide's pinned `LEARN.md` unchanged during normal project work; write new project findings to the designated application log.

## First task to give your agent

```text
Read our ZeyOS project instructions and identify the reference and example files
for this task. Inspect the target entry point and list its required inputs.
Resolve every database field and relationship before writing code.
Make the smallest useful patch and validate without executing it. Report local
checks separately from SDK compilation. Only send source for compilation after
confirming the linked target and permission; otherwise prepare the command.
Do not use zeysdk run, invoke a service, sync, or write business data for this task.
Report any behavior that still needs a separately authorized runtime test.
```

---

Canonical: https://www.zeyos.com/guide/agent-workflows/cli-coverage-and-escalation

# CLI Coverage and Escalation

The CLI is the default interface for coding agents, but it intentionally covers a curated registry instead of the full API surface.

## What the CLI Covers Directly

The command `zeyos resources` is the source of truth for CLI-supported resource types. At the time of writing, the curated registry includes:

| Resource | Operations |
|----------|------------|
| `account`, `actionstep`, `appointment`, `campaign`, `contact`, `document`, `event`, `file`, `invitation`, `item`, `message`, `note`, `opportunity`, `payment`, `project`, `storage`, `task`, `ticket`, `transaction` | `list`, `get`, `create`, `update`, `delete` |
| `customfield` / `customfields` | `list`, `get` |
| `group`, `user` | `list`, `get` |

Plural names and common aliases such as `tickets`, `actionsteps`, `time-entries`, `docs`, `invoice`, and `crm` are resolved by the CLI, but the underlying coverage boundary is still the registry above.

## What the CLI Does Not Try to Cover

Switch away from the CLI when you need:

- Resources that are present in the generated API client but missing from `zeyos resources`
- Low-level request control beyond the registry-backed commands
- Browser session behavior or embedded app flows
- Full access to generated operations such as `listApplications`, `listServices`, `listResources`, `listPermissions`, or `listWeblets`
- Custom auth overrides, raw responses, or path-and-method requests

## Escalation Path

### 1. Stay on the CLI if the resource exists in the registry

This remains the best option for shell automation, quick operational tooling, and JSON-first CRUD workflows.

### 2. Move to `@zeyos/client` for the full generated API surface

```js
import { createZeyosClient, MemoryTokenStore } from '@zeyos/client';

const client = createZeyosClient({
  platform: 'https://cloud.zeyos.com/demo/',
  auth: {
    mode: 'oauth',
    oauth: {
      tokenStore: new MemoryTokenStore({ accessToken: process.env.ZEYOS_TOKEN }),
    },
  },
});

const services = await client.api.listServices({ limit: 20 });
```

Use the JavaScript client whenever the CLI registry is insufficient or the workflow needs request-level control.

### 3. Drop to raw REST/OpenAPI when your runtime is not JavaScript

Use the API reference and OpenAPI files when you are implementing integrations in another language or building a custom SDK.

## Rule of Thumb

- If `zeyos resources` shows the resource and the built-in command shape is enough, stay with the CLI.
- If the resource exists in the API but not in the CLI registry, move to `@zeyos/client`.
- If you are not using JavaScript at all, use the REST/OpenAPI reference directly.

---

Canonical: https://www.zeyos.com/guide/agent-workflows/coding-agents

# Coding Agents

This path is for coding agents, automation scripts, and operational tools working with ZeyOS. Every task starts by choosing one of three routes:

| Route | Use it for | Interfaces |
| --- | --- | --- |
| **Build a ZeyOS app** | Native manifests, services, resources, weblets, hooks and settings | iXML/Zymba, `zeysdk` |
| **Connect an external application** | Software outside ZeyOS that reads or writes it | CLI, MCP, `@zeyos/client`, REST |
| **Use ZeyOS business data** | Questions and explicitly requested record operations | The 15 domain skills, through the CLI or MCP |

Install the `zeyos` router skill, and the agent classifies tasks itself:

```bash
zeyos skills install zeyos --target claude --global
zeyos skills verify --target claude --global
```

An explanation, code edit, review or validation-only task must never trigger a live query, compile, push or run, and loading a skill authorizes nothing. For harness configuration, including OpenCode V2, see [Agent Setup](https://www.zeyos.com/guide/agent-workflows/agent-setup). Native work also uses the [copyable project instructions](https://www.zeyos.com/ai/agent-instructions.md); the separate ZeyOS SDK provides compilation checks without executing the source, and runtime tests are a separate step.

## Start Here

For **Connect an external application** and **Use ZeyOS business data**, start with the CLI:

| Need | Recommended interface | Why |
|------|-----------------------|-----|
| Fast CRUD against common business resources | [`zeyos`](https://www.zeyos.com/guide/cli/getting-started) | Human-readable help, JSON output, built-in auth flow, safe delete confirmation |
| Reliable shell automation and pipelines | [`zeyos --json`](https://www.zeyos.com/guide/agent-workflows/agent-quickstart) | Stable machine-readable output, easy to combine with `jq` and scripts |
| Access beyond the CLI's curated resource registry | [`@zeyos/client`](https://www.zeyos.com/guide/javascript-client/getting-started) | Covers the full generated API surface |
| Non-JavaScript or custom HTTP clients | [REST/OpenAPI](https://www.zeyos.com/guide/api-reference/resources) | Raw endpoint and schema reference |

## Recommended Route

1. Follow the [Agent Quickstart](https://www.zeyos.com/guide/agent-workflows/agent-quickstart) to authenticate and make your first read/write calls.
2. Use [Agent Recipes](https://www.zeyos.com/guide/agent-workflows/agent-recipes) for common ticket, account, task, and project workflows.
3. Check [CLI Coverage and Escalation](https://www.zeyos.com/guide/agent-workflows/cli-coverage-and-escalation) whenever you need a resource or request shape the CLI does not expose directly.

## Working Rules

- Prefer `--json` for agent-driven flows.
- Prefer `filters`-style JSON payloads in CLI examples so the transition to `@zeyos/client` stays consistent.
- Use `visibility: 0` on resources that **have** the column — `accounts`, `contacts`, `tickets`, `tasks`, `projects`, `items`, `documents`, `notes`, `opportunities`, `appointments`, `campaigns`, `mailinglists`, `storages`, `contracts`, `ledgers` — unless you want archived (`1`) or deleted (`2`) records. **Most other resources have no `visibility` column** — `transactions` (and every billing/procurement entity), `payments`, `messages`, `actionsteps`, `addresses`, `users`, `prices`, `dunning` — and filtering on it there returns an opaque HTTP 400. Check with `zeyos describe <entity>` when unsure.
- Treat `delete` as destructive. The CLI prompts by default; use `--force` only in deliberate automation.
- Switch to the JavaScript client when the task needs unsupported resources, browser/session behavior, or low-level request control.

---

Canonical: https://www.zeyos.com/guide/api-reference/authentication

# Authentication

The ZeyOS API supports multiple authentication methods to accommodate different use cases -- from server-side integrations to browser-based applications. This guide covers OAuth2 token-based authentication, session cookie authentication, and the helpers provided by the `@zeyos/client` library.

## Authentication Overview

ZeyOS exposes an OAuth2-compatible authorization server at:

```
https://cloud.zeyos.com/{INSTANCE}/oauth2/v1/
```

This endpoint supports the standard OAuth 2.0 Authorization Code flow, token refresh, token revocation, and token introspection. For browser applications where the user is already logged into ZeyOS, session cookie authentication is also available.

## OAuth 2.0 Authorization Code Flow

The Authorization Code flow is the recommended approach for server-side applications and other trusted integrations that need to act on behalf of a user.

### Step 1: Build the Authorization URL

Redirect the user to the ZeyOS authorization endpoint to request access. The JavaScript example below assumes a trusted environment that can safely hold OAuth client credentials:

```js
import { createZeyosClient } from '@zeyos/client';

const client = createZeyosClient({
  platform: 'https://cloud.zeyos.com/demo',
  auth: {
    oauth: {
      clientId: 'YOUR_CLIENT_ID',
      clientSecret: 'YOUR_CLIENT_SECRET',
    },
  },
});

const authUrl = client.oauth2.buildAuthorizationUrl({
  redirectUri: 'https://yourapp.example.com/callback',
  state: 'random-csrf-token',
});

// Redirect the user to authUrl
```

The authorization URL follows this format:

```
https://cloud.zeyos.com/{INSTANCE}/oauth2/v1/authorize
  ?client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.example.com/callback
  &response_type=code
  &state=random-csrf-token
```

### Step 2: User Authorizes

The user logs in (if not already authenticated) and approves access to your application. ZeyOS redirects the user back to your `redirect_uri` with an authorization code:

```
https://yourapp.example.com/callback?code=AUTHORIZATION_CODE&state=random-csrf-token
```

### Step 3: Exchange the Code for Tokens

Exchange the authorization code for an access token and refresh token:

**Using the JavaScript client:**

```js
const callback = client.oauth2.parseAuthorizationCallback(callbackUrl);

if (callback.isError) {
  console.error('Authorization failed:', callback.errorDescription);
} else {
  const tokenSet = await client.oauth2.exchangeAuthorizationCode({
    code: callback.code,
    redirectUri: 'https://yourapp.example.com/callback',
  });

  console.log('Access token:', tokenSet.accessToken);
  console.log('Refresh token:', tokenSet.refreshToken);
}
```

**Using curl:**

```bash
curl -X POST "https://cloud.zeyos.com/demo/oauth2/v1/token" \
  -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=AUTHORIZATION_CODE" \
  -d "redirect_uri=https://yourapp.example.com/callback"
```

**Response:**

```json
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g..."
}
```

### Step 4: Use the Bearer Token

Include the access token in the `Authorization` header for all API requests:

```bash
curl -X POST "https://cloud.zeyos.com/demo/api/v1/tickets" \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{"limit": 10}'
```

With the JavaScript client, tokens are managed automatically once stored:

```js
const tickets = await client.api.listTickets({ limit: 10 });
```

## Token Refresh

Access tokens expire after a limited time (indicated by `expires_in` in the token response). Use the refresh token to obtain a new access token without requiring user interaction.

**Using the JavaScript client:**

```js
const newTokenSet = await client.oauth2.refreshToken();
```

:::tip
When you configure `autoRefresh: true` in the client's OAuth settings, expired access tokens are refreshed automatically before each request. This requires `clientId` and `clientSecret`, so treat it as a trusted-environment pattern.
:::

**Using curl:**

```bash
curl -X POST "https://cloud.zeyos.com/demo/oauth2/v1/token" \
  -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=dGhpcyBpcyBhIHJlZnJlc2g..."
```

## Token Revocation

Revoke a token when the user logs out or your application no longer needs access:

**Using the JavaScript client:**

```js
await client.oauth2.revokeToken({
  token: tokenSet.accessToken,
});
```

**Using curl:**

```bash
curl -X POST "https://cloud.zeyos.com/demo/oauth2/v1/revoke" \
  -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=eyJhbGciOiJSUzI1NiIs..."
```

## Session Authentication

For browser-based applications where the user is already logged into ZeyOS, session cookie authentication provides a seamless experience without requiring a separate OAuth flow.

### How It Works

When a user is logged into ZeyOS in their browser, ZeyOS sets a session cookie (`ZEYOSID`). By including `credentials: 'include'` in fetch requests, the browser automatically sends this cookie, authenticating the request.

### Client Configuration

```js
const client = createZeyosClient({
  platform: 'https://cloud.zeyos.com/demo',
  auth: {
    mode: 'session',
    session: {
      enabled: true,
      credentials: 'include',
    },
  },
});
```

### Checking Session Status

You can verify whether the user has an active session by calling the `userinfo` endpoint:

```js
const endpoint = 'https://cloud.zeyos.com/demo/oauth2/v1/userinfo';
const response = await fetch(endpoint, {
  method: 'GET',
  credentials: 'include',
  headers: { 'Accept': 'application/json' },
});

if (response.ok) {
  const userInfo = await response.json();
  console.log('Logged in as:', userInfo.sub);
}
```

:::info
Session authentication only works in browser environments where the user is on the same domain (or an allowed origin) as the ZeyOS instance. It will not work in Node.js or server-side contexts.
:::

:::warning
Do not embed `clientSecret` in shipped browser-only code. For browser apps, prefer session-cookie authentication or perform the OAuth code exchange and refresh on a backend that stores the client credentials safely.
:::

## Security Schemes

The ZeyOS API supports three authentication schemes. Each API operation declares which schemes it accepts:

| Scheme | Type | Transport | Use Case |
|--------|------|-----------|----------|
| **Bearer** (OAuth2) | Token | `Authorization: Bearer {token}` | Server-side apps, SPAs, CLI tools |
| **Session** (Cookie) | Cookie | `Cookie: ZEYOSID={session_id}` | Browser apps where user is logged into ZeyOS |
| **Basic** (Client Credentials) | Header | `Authorization: Basic {base64}` | OAuth2 token endpoint requests |

Most API resource operations accept both **Bearer** and **Session** authentication. The OAuth2 token endpoints (`/token`, `/revoke`, `/introspect`) use **Basic** authentication with your client credentials.

The `@zeyos/client` library handles scheme selection automatically based on your configuration:

- `mode: 'auto'` (default) -- Tries bearer first if tokens are available, falls back to session.
- `mode: 'oauth'` -- Uses bearer tokens exclusively.
- `mode: 'session'` -- Uses session cookies exclusively.
- `mode: 'none'` -- No authentication (for public endpoints).

:::warning
Never expose your `client_secret` in client-side JavaScript. For browser-based SPAs, use the session authentication mode or implement a server-side proxy for the token exchange. Keep OAuth client credentials on your backend server where they cannot be inspected by end users.
:::

## Token Management

The client library provides built-in token storage and management:

```js
// Read the current token set
const tokenSet = await client.auth.getTokenSet();

// Manually set tokens (e.g., after loading from a database)
await client.auth.setTokenSet({
  accessToken: 'new-access-token',
  refreshToken: 'new-refresh-token',
  expiresAt: Math.floor(Date.now() / 1000) + 3600,
});

// Clear all stored tokens (logout)
await client.auth.clearTokenSet();
```

For custom persistence (e.g., storing tokens in a database or encrypted file), implement the `TokenStore` interface:

```js
const customStore = {
  async get() {
    // Return the stored token set or null
    return loadFromDatabase();
  },
  async set(tokenSet) {
    // Persist the token set (or null to clear)
    await saveToDatabase(tokenSet);
  },
};

const client = createZeyosClient({
  platform: 'https://cloud.zeyos.com/demo',
  auth: {
    oauth: {
      tokenStore: customStore,
      clientId: 'YOUR_CLIENT_ID',
      clientSecret: 'YOUR_CLIENT_SECRET',
      autoRefresh: true,
    },
  },
});
```

---

Canonical: https://www.zeyos.com/guide/api-reference/data-retrieval

# Data Retrieval

The ZeyOS REST API provides a flexible query language for retrieving data from any resource endpoint. You can control exactly which fields are returned, apply composite filters, search by keyword, sort results, paginate through large datasets, expand JSON and binary columns, and access extended data fields.

:::info HTTP Method
All list operations use **POST** requests with a JSON body, not GET. This is because queries can include complex filters, field selections, and composite expressions that would be impractical as URL query parameters.
:::

## Field Selection

By default, list endpoints return all standard fields for a resource. Use the `fields` parameter to request only the columns you need, reducing payload size and improving performance.

### Array Form

Pass a simple array of field names to select specific columns:

```json
{
  "fields": ["ID", "name", "status", "priority"]
}
```

**JavaScript client:**

```js
const result = await client.api.listTickets({
  fields: ['ID', 'name', 'status', 'priority'],
});
```

### Object Form with Aliases

Use an object to rename fields in the response. Keys become the output names; values are the source field paths:

```json
{
  "fields": {
    "Id": "ID",
    "Name": "lastname",
    "Nickname": "extdata.nickname",
    "Address": "contact.address",
    "Postalcode": "contact.postalcode",
    "Town": "contact.city",
    "SalesAgent": "assigneduser.name"
  }
}
```

**JavaScript client:**

```js
const result = await client.api.listAccounts({
  fields: {
    Id: 'ID',
    Name: 'lastname',
    Nickname: 'extdata.nickname',
    Address: 'contact.address',
    Postalcode: 'contact.postalcode',
    Town: 'contact.city',
    SalesAgent: 'assigneduser.name',
  },
});
```

### Dot-Notation Joins

Use dot notation to access fields on related records without a separate request. You can discover available relationships in the [Schema Reference](https://www.zeyos.com/guide/api-reference/schema).

| Notation | Description |
|----------|-------------|
| `contact.city` | City field from the linked contact record |
| `contact.address` | Address from the linked contact |
| `contact.postalcode` | Postal code from the linked contact |
| `contact.country` | Country from the linked contact |
| `assigneduser.name` | Name of the assigned user |
| `account.lastname` | Last name (or company name) of the linked account |
| `project.name` | Name of the linked project |

### Extended Data Fields via Dot-Notation

Access custom fields stored in a record's `extdata` JSON column using the `extdata.` prefix in your field selection:

```json
{
  "fields": {
    "Nickname": "extdata.nickname",
    "Department": "extdata.department"
  }
}
```

:::info
Extended data fields are user-defined and may vary between ZeyOS instances. Whenever you create a new form field in ZeyOS, the field's value is stored in `extdata`. Check your instance configuration for available custom fields.
:::

## Filters

The `filter` parameter lets you restrict results using comparison operators, string-matching operators, and composite logical expressions.

:::tip JavaScript Client: use `filters` (plural)
The bundled OpenAPI spec names this parameter `filter` (singular), but ZeyOS's published API documentation uses `filters` (plural) in every worked example — and that is what the JavaScript client and CLI send. Use `filters`. See the [Practical Guide](https://www.zeyos.com/guide/javascript-client/practical-guide#always-use-filters-plural).
:::

### Filter Syntax

Filters are expressed as an object where keys are field names and values define the match condition:

```
filter = {
  "field": "value",
  "field2": {"=": "value"},
  "field3": {"<": "value1", ">": "value2"},
  "field4": {"IN": ["value1", "value2"]},
  N: ["AND/OR/NOT", {...}, {...}]
}
```

### Simple Equality

Pass a field name with a plain value for equality matching:

```json
{
  "filter": {
    "status": 1,
    "visibility": 0
  }
}
```

### Comparison Operators

Use an object value with operator keys for more advanced comparisons:

```json
{
  "filter": {
    "priority": {">=": 3},
    "amount": {">": 100, "<": 1000}
  }
}
```

The full set of comparison operators:

| Operator | Description |
|----------|-------------|
| `=` | Equal to |
| `!=` or `<>` | Not equal to |
| `<` | Less than |
| `<=` | Less than or equal to |
| `>` | Greater than |
| `>=` | Greater than or equal to |
| `IN` | Value is in the given set |
| `!IN` | Value is not in the given set |

**IN operator example:**

```json
{
  "filter": {
    "contact.country": {"IN": ["DE", "AT", "GB"]}
  }
}
```

### String Operators

For string fields, additional pattern-matching operators are available:

| Operator | Description | Case Sensitive |
|----------|-------------|:-:|
| `~` | Matches regular expression | Yes |
| `~*` | Matches regular expression | No |
| `!~` | Does not match regular expression | Yes |
| `!~*` | Does not match regular expression | No |
| `~~` | LIKE (pattern match) | Yes |
| `~~*` | LIKE (pattern match) | No |
| `!~~` | NOT LIKE | Yes |
| `!~~*` | NOT LIKE | No |

:::tip
The `~` operators test regular expressions, while `~~` operators use SQL-style LIKE patterns. The `*` suffix makes any operator case-insensitive.
:::

### Composite Filters (AND, OR, NOT)

Combine multiple conditions using logical operators. Composite filters use **numbered keys** in the filter object to include logical groups alongside simple field conditions:

```json
{
  "filter": {
    "visibility": 0,
    "contact.country": {"IN": ["DE", "AT", "GB"]},
    "2": ["OR",
      {"lastmodified": {">": 1524472045}},
      {"contact.lastmodified": {">": 1524472045}}
    ]
  }
}
```

In this example:
- `"visibility": 0` is a simple equality filter.
- `"contact.country": {"IN": [...]}` uses the IN operator on a joined field.
- The key `"2"` contains an OR group: the record matches if either its own `lastmodified` or its contact's `lastmodified` exceeds the given timestamp.

You can nest logical operators as deeply as needed:

```json
{
  "filter": {
    "0": ["AND",
      {"status": {"IN": [1, 2]}},
      {"visibility": 0},
      {"1": ["OR",
        {"priority": {">=": 3}},
        {"duedate": {"<": "2025-06-01"}}
      ]}
    ]
  }
}
```

## Search Queries

The `query` parameter performs a search across all searchable string fields for a resource (typically `name`, `description`, and similar columns):

```json
{
  "query": "server outage"
}
```

You can combine `query` with `filter` to narrow results further:

```json
{
  "query": "payment",
  "filter": {"status": 1}
}
```

**JavaScript client:**

```js
const results = await client.api.listTickets({
  query: 'server outage',
  filters: { status: 1 },
});
```

## Sorting

Control the order of results using the `sort` parameter. Provide an array of field names, each prefixed with `+` for ascending or `-` for descending:

```json
{
  "sort": ["+lastname", "-contact.country"]
}
```

| Prefix | Direction | Example |
|--------|-----------|---------|
| `+` | Ascending (A-Z, 0-9, oldest first) | `"+lastname"` |
| `-` | Descending (Z-A, 9-0, newest first) | `"-lastmodified"` |

Multiple sort fields are applied in order -- the example above sorts by `lastname` ascending first, then by `contact.country` descending within each name.

:::tip
Sort by `"-lastmodified"` to get the most recently updated records first. This is a common default for dashboard-style views.
:::

## Distinct Results

The `distinct` parameter eliminates duplicate rows from the result set. This is useful when a query returns multiple rows for the same record due to joins or multi-value fields.

```json
{
  "distinct": true,
  "fields": ["ID", "lastname", "contact.country"],
  "filter": { "visibility": 0 }
}
```

**JavaScript client:**

```js
const result = await client.api.listAccounts({
  distinct: true,
  fields: ['ID', 'lastname', 'contact.country'],
  filters: { visibility: 0 },
});
```

## Pagination

The API supports offset-based pagination using `limit` and `offset`:

| Parameter | Type | Default | Max | Description |
|-----------|------|---------|-----|-------------|
| `limit` | integer | `1000` | `10000` | Maximum number of records to return |
| `offset` | integer | `0` | — | Number of records to skip before returning results |

```json
{
  "limit": 25,
  "offset": 50
}
```

:::info Default limit
When no `limit` is specified, the API returns up to **1000** records. The maximum supported value is **10000**. For large datasets, always paginate using `limit` and `offset`.
:::

### Getting the Total Count

To retrieve the total number of matching records without fetching the data itself, set `count` to `1`. The response will contain only the count:

**Request:**

```json
{
  "count": true,
  "filter": {
    "visibility": 0,
    "contact.country": {"IN": ["DE", "AT", "GB"]},
    "2": ["OR",
      {"lastmodified": {">": 1524472045}},
      {"contact.lastmodified": {">": 1524472045}}
    ]
  }
}
```

**Response:**

```json
{
  "count": 5
}
```

**JavaScript client:**

```js
const countResult = await client.api.listAccounts({
  count: true,
  filters: { visibility: 0 },
});
// countResult => { count: 5 }
```

:::info
Use `count` to build pagination controls in your UI. First request the count to determine the total number of pages, then paginate through results using `limit` and `offset`. Higher-level client helpers may normalize count-enabled responses differently, so keep the raw API shape and the client-layer shape conceptually separate.
:::

## Expanding JSON and Binary Data

Some table columns contain JSON data or reference binary files. By default, these columns are not expanded in list responses. The `expand` parameter tells the API to load and inline the column's content automatically.

:::warning
The `expand` parameter is **only** for JSON data columns (like `items` in transactions or `data` in objects) and binary file references (like `binfile`). It is **not** used for extended data fields -- see [Extended Data (extdata)](#extended-data-extdata) below.
:::

**Example -- expanding a binary file column:**

```json
{
  "fields": ["ID", "subject", "binfile"],
  "expand": ["binfile"],
  "limit": 1
}
```

**curl:**

```bash
curl -X POST "https://cloud.zeyos.com/demo/api/v1/messages/" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "fields": ["ID", "subject", "binfile"],
    "expand": ["binfile"],
    "limit": 1
  }'
```

**Response:**

```json
[{"ID": 188, "subject": "Test", "binfile": {"content": "UmV0dXJuLVBhdGg6IDx..."}}]
```

The binary content is returned inline as base64-encoded data. In the example above, the `binfile` column is expanded to include the full email message content as [RFC 822](https://www.ietf.org/rfc/rfc822.txt).

Other common use cases for `expand`:
- `items` in transaction records (invoice line items as JSON)
- `data` in object records (structured JSON data)

## Extended Data (extdata)

Extended data (`extdata`) is a concept in ZeyOS that allows storing additional custom values for any entity. Whenever you create a new form field in ZeyOS, the field's value is stored in `extdata`. Accessing extdata is **separate** from the `expand` parameter.

### In List Requests (POST)

There are two ways to include extended data in list responses:

**Option 1: Select specific extdata fields via dot-notation in `fields`:**

```json
{
  "fields": {
    "Id": "ID",
    "Name": "lastname",
    "Nickname": "extdata.nickname",
    "Department": "extdata.department"
  }
}
```

This approach lets you pick individual extdata fields and give them aliases.

**Option 2: Include all extdata using the `extdata` body parameter:**

```json
{
  "fields": ["ID", "lastname", "status"],
  "extdata": 1,
  "limit": 10
}
```

**JavaScript client:**

```js
// Select specific extdata fields
const result = await client.api.listAccounts({
  fields: {
    Id: 'ID',
    Name: 'lastname',
    Nickname: 'extdata.nickname',
  },
});

// Or include all extdata
const result = await client.api.listAccounts({
  fields: ['ID', 'lastname', 'status'],
  extdata: 1,
  limit: 10,
});
```

### In GET Requests

When fetching a single record, pass `extdata=1` as a query parameter:

```bash
curl "https://cloud.zeyos.com/demo/api/v1/accounts/42?extdata=1" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

**JavaScript client:**

```js
const account = await client.api.getAccount({
  ID: 42,
  extdata: 1,
});
```

## Complete Example

Here is the canonical example from the ZeyOS documentation, combining field selection with aliases, dot-notation joins, extdata fields, composite filters, sorting, and pagination.

### Query

```json
{
  "fields": {
    "Id": "ID",
    "Name": "lastname",
    "Nickname": "extdata.nickname",
    "Address": "contact.address",
    "Postalcode": "contact.postalcode",
    "Town": "contact.city",
    "SalesAgent": "assigneduser.name"
  },
  "filter": {
    "visibility": 0,
    "contact.country": {"IN": ["DE", "AT", "GB"]},
    "2": ["OR",
      {"lastmodified": {">": 1524472045}},
      {"contact.lastmodified": {">": 1524472045}}
    ]
  },
  "sort": ["+lastname", "-contact.country"],
  "limit": 3,
  "offset": 0
}
```

### Using curl

```bash
curl -X POST "https://cloud.zeyos.com/demo/api/v1/accounts/" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "fields": {
      "Id": "ID",
      "Name": "lastname",
      "Nickname": "extdata.nickname",
      "Address": "contact.address",
      "Postalcode": "contact.postalcode",
      "Town": "contact.city",
      "SalesAgent": "assigneduser.name"
    },
    "filter": {
      "visibility": 0,
      "contact.country": {"IN": ["DE", "AT", "GB"]},
      "2": ["OR",
        {"lastmodified": {">": 1524472045}},
        {"contact.lastmodified": {">": 1524472045}}
      ]
    },
    "sort": ["+lastname", "-contact.country"],
    "limit": 3,
    "offset": 0
  }'
```

### Using the JavaScript Client

```js
const accounts = await client.api.listAccounts({
  fields: {
    Id: 'ID',
    Name: 'lastname',
    Nickname: 'extdata.nickname',
    Address: 'contact.address',
    Postalcode: 'contact.postalcode',
    Town: 'contact.city',
    SalesAgent: 'assigneduser.name',
  },
  filters: {
    visibility: 0,
    'contact.country': { IN: ['DE', 'AT', 'GB'] },
    2: [
      'OR',
      { lastmodified: { '>': 1524472045 } },
      { 'contact.lastmodified': { '>': 1524472045 } },
    ],
  },
  sort: ['+lastname', '-contact.country'],
  limit: 3,
  offset: 0,
});
```

### Response

```json
[
  {
    "Id": 2,
    "Name": "BEQ Building Equipment",
    "Nickname": null,
    "Address": "Queensstreet",
    "Postalcode": "12923",
    "Town": "London",
    "SalesAgent": "Max Mueller"
  },
  {
    "Id": 15,
    "Name": "CleanTexx",
    "Nickname": null,
    "Address": "Tower Bridge",
    "Postalcode": "12923",
    "Town": "London",
    "SalesAgent": null
  },
  {
    "Id": 1,
    "Name": "Lightexx AG",
    "Nickname": null,
    "Address": "Schmittstr. 4",
    "Postalcode": "80172",
    "Town": "Munich",
    "SalesAgent": null
  }
]
```

:::tip
When building interactive UIs, start with a `count` request to determine the total result set size, then use `limit` and `offset` to implement paginated loading.
:::

---

Canonical: https://www.zeyos.com/guide/api-reference/resources

# Resources

The ZeyOS REST API exposes a comprehensive set of resources covering CRM, project management, ticketing, commerce, communication, and more. Each resource maps to a RESTful endpoint under the base URL and supports a consistent set of operations.

:::info API Surface vs. CLI Surface
This page documents the **full API surface** exposed by ZeyOS. The CLI intentionally supports a curated subset of common resources. For the CLI boundary and escalation path, see [CLI Coverage and Escalation](https://www.zeyos.com/guide/agent-workflows/cli-coverage-and-escalation).
:::

## Available Resources

The table below lists all available API resources and their supported operations.

| Resource | Endpoint | List | Get | Create | Update | Delete |
|----------|----------|:----:|:---:|:------:|:------:|:------:|
| Accounts | `/accounts` | Yes | Yes | Yes | Yes | Yes |
| ActionSteps | `/actionsteps` | Yes | Yes | Yes | Yes | Yes |
| Addresses | `/addresses` | Yes | Yes | Yes | Yes | Yes |
| Applications | `/applications` | Yes | Yes | -- | -- | -- |
| ApplicationAssets | `/applicationassets` | Yes | Yes | -- | -- | -- |
| Appointments | `/appointments` | Yes | Yes | Yes | Yes | Yes |
| Associations | `/associations` | Yes | Yes | Yes | Yes | Yes |
| BinFiles | `/binfiles` | Yes | -- | -- | -- | -- |
| Campaigns | `/campaigns` | Yes | Yes | Yes | Yes | Yes |
| Categories | `/categories` | Yes | Yes | Yes | Yes | Yes |
| Channels | `/channels` | Yes | Yes | Yes | Yes | Yes |
| Comments | `/comments` | Yes | Yes | Yes | Yes | Yes |
| Components | `/components` | Yes | Yes | Yes | Yes | Yes |
| Contacts | `/contacts` | Yes | Yes | Yes | Yes | Yes |
| ContactsToContacts | `/contactstocontacts` | Yes | Yes | Yes | Yes | Yes |
| Contracts | `/contracts` | Yes | Yes | Yes | Yes | Yes |
| Coupons | `/coupons` | Yes | Yes | Yes | Yes | Yes |
| CouponCodes | `/couponcodes` | Yes | Yes | Yes | Yes | Yes |
| CustomFields | `/customfields` | Yes | Yes | -- | -- | -- |
| DAVServers | `/davservers` | Yes | Yes | Yes | Yes | Yes |
| Devices | `/devices` | Yes | Yes | Yes | Yes | Yes |
| Documents | `/documents` | Yes | Yes | Yes | Yes | Yes |
| DunningNotices | `/dunningnotices` | Yes | Yes | Yes | Yes | Yes |
| DunningToTransactions | `/dunningtotransactions` | Yes | Yes | Yes | Yes | Yes |
| EntitiesToChannels | `/entitiestochannels` | Yes | Yes | Yes | Yes | Yes |
| Events | `/events` | Yes | Yes | Yes | Yes | Yes |
| FeedServers | `/feedservers` | Yes | Yes | Yes | Yes | Yes |
| Files | `/files` | Yes | Yes | Yes | Yes | Yes |
| Follows | `/follows` | Yes | Yes | Yes | Yes | Yes |
| Forks | `/forks` | Yes | Yes | -- | -- | -- |
| Groups | `/groups` | Yes | Yes | -- | -- | -- |
| GroupsToUsers | `/groupstousers` | Yes | Yes | -- | -- | -- |
| Invitations | `/invitations` | Yes | Yes | Yes | Yes | Yes |
| Items | `/items` | Yes | Yes | Yes | Yes | Yes |
| Ledgers | `/ledgers` | Yes | Yes | Yes | Yes | Yes |
| Likes | `/likes` | Yes | Yes | Yes | Yes | Yes |
| Links | `/links` | Yes | Yes | Yes | Yes | Yes |
| MailingLists | `/mailinglists` | Yes | Yes | Yes | Yes | Yes |
| MailingRecipients | `/mailingrecipients` | Yes | Yes | Yes | Yes | Yes |
| MailServers | `/mailservers` | Yes | Yes | Yes | Yes | Yes |
| Messages | `/messages` | Yes | Yes | Yes | Yes | Yes |
| MessageReads | `/messagereads` | Yes | Yes | Yes | Yes | Yes |
| Notes | `/notes` | Yes | Yes | Yes | Yes | Yes |
| Objects | `/objects` | Yes | Yes | Yes | Yes | Yes |
| Opportunities | `/opportunities` | Yes | Yes | Yes | Yes | Yes |
| Participants | `/participants` | Yes | Yes | Yes | Yes | Yes |
| Payments | `/payments` | Yes | Yes | Yes | Yes | Yes |
| Permissions | `/permissions` | Yes | Yes | -- | -- | -- |
| PriceLists | `/pricelists` | Yes | Yes | Yes | Yes | Yes |
| PriceListsToAccounts | `/priceliststoaccounts` | Yes | Yes | Yes | Yes | Yes |
| Prices | `/prices` | Yes | Yes | Yes | Yes | Yes |
| Projects | `/projects` | Yes | Yes | Yes | Yes | Yes |
| Records | `/records` | Yes | Yes | Yes | Yes | Yes |
| RelatedItems | `/relateditems` | Yes | Yes | Yes | Yes | Yes |
| Resources | `/resources` | Yes | Yes | -- | -- | -- |
| Services | `/services` | Yes | Yes | -- | -- | -- |
| StockTransactions | `/stocktransactions` | Yes | Yes | Yes | Yes | Yes |
| Storages | `/storages` | Yes | Yes | Yes | Yes | Yes |
| Suppliers | `/suppliers` | Yes | Yes | Yes | Yes | Yes |
| Tasks | `/tasks` | Yes | Yes | Yes | Yes | Yes |
| Tickets | `/tickets` | Yes | Yes | Yes | Yes | Yes |
| Transactions | `/transactions` | Yes | Yes | Yes | Yes | Yes |
| Users | `/users` | Yes | Yes | -- | -- | -- |
| Weblets | `/weblets` | Yes | Yes | -- | -- | -- |

## Common Resources

The following resources are the most frequently used when building integrations with ZeyOS.

### Tickets

Support and service tickets with status tracking, priority levels, due dates, and assignment to users. Tickets are the central work item in ZeyOS and can be linked to accounts, projects, contacts, and tasks.

```js
// List open tickets sorted by priority
const tickets = await client.api.listTickets({
  filters: { visibility: 0, status: 4 },
  sort: ['-priority', '+duedate'],
  limit: 50,
});

// Get a single ticket with extended data
const ticket = await client.api.getTicket({ ID: 12345, extdata: 1, tags: 1 });

// Create a new ticket
const newTicket = await client.api.createTicket({
  name: 'Server maintenance required',
  priority: 3,
  status: 0,
});

// Update a ticket — flat spread form or explicit body key both work
await client.api.updateTicket({ ID: 12345, status: 4, priority: 4 });
// Equivalent using explicit body key:
// await client.api.updateTicket({ ID: 12345, body: { status: 4, priority: 4 } });
```

### Accounts

Customer and company records that serve as the primary organizational entity in the CRM. Accounts can have linked contacts, tickets, documents, and transactions.

```js
const accounts = await client.api.listAccounts({
  fields: ['ID', 'lastname', 'firstname'],
  filters: { visibility: 0 },
  sort: ['+lastname'],
  limit: 100,
});
```

### Contacts

Individual people linked to accounts. Contacts store personal details such as name, email, phone, and address information.

```js
const contacts = await client.api.listContacts({
  fields: {
    Id: 'ID',
    Name: 'lastname',
    Email: 'email',
    Company: 'account.lastname',
  },
  filters: { visibility: 0 },
  sort: ['+lastname'],
});
```

### Tasks

Actionable work items that can be linked to tickets, projects, or other entities. Tasks track completion status and can be assigned to users.

```js
const tasks = await client.api.listTasks({
  filters: { ticket: 12345, visibility: 0 },
  sort: ['+name'],
  limit: 200,
});
```

### Projects

Organizational groupings for tickets, tasks, and other work items. Projects provide a way to plan and track larger initiatives.

```js
const projects = await client.api.listProjects({
  fields: ['ID', 'name'],
  filters: { visibility: 0 },
  sort: ['+name'],
});
```

### Items

Products and services with pricing information. Items are referenced in documents (invoices, quotes) and can be linked to price lists, categories, and suppliers.

```js
const items = await client.api.listItems({
  fields: ['ID', 'name', 'price', 'unit'],
  sort: ['+name'],
  limit: 100,
});
```

### Documents

Stored files and document records — attachments, generated PDFs and similar — linked to
accounts and other records. Fields include `name`, `documentnum`, `filename` and `status`.

```js
const recent = await client.api.listDocuments({
  filters: { visibility: 0 },
  sort: ['-lastmodified'],
  limit: 25,
});
```

:::warning Invoices are not documents
Invoices, quotes, orders, credits and delivery notes are **transactions**, distinguished by
`transactions.type` — an invoice is `type: 3`. The `documents` resource has no `doctype`
column, so filtering on one returns an HTTP 400.

```js
const invoices = await client.api.listTransactions({
  filters: { type: 3 },
  sort: ['-date'],
  limit: 25,
});
```

On the CLI, use the entity that binds the type for you:

```bash
zeyos list billing_invoices --limit 25 --sort -date
```

See the CLI's
[transaction entities](https://www.zeyos.com/guide/cli/commands#list).
:::

## Resource Naming

The API follows consistent naming conventions for endpoints and operations:

### Endpoint Paths

Resource endpoints use **plural, lowercase** paths:

```
/accounts
/tickets
/contacts
/documents
```

Individual records are accessed by appending `/{ID}`:

```
/accounts/{ID}
/tickets/{ID}
```

### Operation IDs

The JavaScript client uses camelCase operation IDs that follow a verb-noun pattern:

| Operation | Pattern | Example |
|-----------|---------|---------|
| List | `list{Resources}` | `listTickets`, `listAccounts` |
| Get | `get{Resource}` | `getTicket`, `getAccount` |
| Create | `create{Resource}` | `createTicket`, `createAccount` |
| Update | `update{Resource}` | `updateTicket`, `updateAccount` |
| Delete | `delete{Resource}` | `deleteTicket`, `deleteAccount` |
| Exists | `exists{Resource}` | `existsTicket`, `existsAccount` |

All operations are available as methods on the `client.api` object:

```js
// List operations accept query parameters (fields, filter, sort, etc.)
const results = await client.api.listTickets({ limit: 10 });

// Get operations require an ID path parameter
const ticket = await client.api.getTicket({ ID: 123 });

// Create operations accept a request body
const created = await client.api.createTicket({ name: 'New ticket' });

// Update operations require an ID and changed fields (flat spread or explicit body key)
await client.api.updateTicket({ ID: 123, status: 4 });

// Delete operations require an ID
await client.api.deleteTicket({ ID: 123 });

// Exists operations return a boolean-like HEAD response
await client.api.existsTicket({ ID: 123 });
```

:::info
The client library is auto-generated from the OpenAPI specification files located in the `/openapi/` directory of this project. The spec files (`api.json`, `oauth2.json`, `auth.json`) define every available resource, operation, parameter, and response schema. Run `npm run generate` to regenerate the client after modifying the spec files.
:::

## HTTP Methods

The API uses the following HTTP methods, which may differ from typical REST conventions:

| Operation | HTTP Method | Description |
|-----------|-------------|-------------|
| List | `POST` | Retrieve a filtered list of records (body contains query parameters) |
| Get | `GET` | Retrieve a single record by ID |
| Create | `PUT` | Create a new record |
| Update | `PATCH` | Partially update an existing record |
| Delete | `DELETE` | Delete a record by ID |
| Exists | `HEAD` | Check if a record exists (returns no body) |

:::tip
Note that **list** operations use `POST` rather than `GET`. This is because the query language (filters, field selection, sorting) can produce request payloads that exceed URL length limits. Using `POST` allows complex queries to be sent reliably in the request body.
:::

---

Canonical: https://www.zeyos.com/guide/api-reference/schema

# Schema Reference

This page documents the fields, types, and relationships for the most commonly used ZeyOS resources. Use it as a quick lookup when building queries, creating records, or planning data integrations.

## Conventions

- **Timestamps** are Unix timestamps in **seconds** (not milliseconds). Convert with `new Date(value * 1000)` in JavaScript.
- **Visibility** controls soft-delete behaviour: `0` = regular, `1` = archived, `2` = deleted. Include `visibility: 0` to exclude archived and deleted records — but only on resources that actually have the column. It is absent from `transactions` and every billing/procurement entity, `payments`, `messages`, `actionsteps`, `addresses` and `users`, where filtering on it returns an opaque HTTP 400. `zeyos describe <entity>` lists the columns a resource really has.
- **GIN-indexed fields** (foreign keys such as `project`, `account`, `ticket`) are filtered the same way as any other column. Pass every filter as `filters` (plural) — see the [Practical Guide](https://www.zeyos.com/guide/javascript-client/practical-guide#always-use-filters-plural).
- Fields marked **(required)** are `NOT NULL` in the database. Most of them have a server-side default (for example `creator` defaults to the authenticated user, `creationdate`/`lastmodified` default to the current time, integer flags default to `0`), so a create that omits them still succeeds. The exception to watch is **`currency` on accounts**: it is `NOT NULL` with no default, so `createAccount` without it fails with an opaque HTTP 500. On updates (PATCH), only the fields you send are changed.
- **Foreign key fields** (e.g. `account`, `project`, `ticket`) accept integer IDs referencing the related resource.

---

### Accounts

| Field | Type | Description |
|-------|------|-------------|
| `ID` | integer | Account ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `assigneduser` | integer | Assigned user ID |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `contact` | integer | Contact ID |
| `visibility` | integer | Visibility (`0`=REGULAR, `1`=ARCHIVED, `2`=DELETED) (required) |
| `lastname` | text | Last name (surname or company name); required if `firstname` is empty (required) |
| `firstname` | text | First name (given name); required if `lastname` is empty (required) |
| `type` | integer | Account type (`0`=PROSPECT, `1`=CUSTOMER, `2`=SUPPLIER, `3`=CUSTOMERANDSUPPLIER, `4`=COMPETITOR, `5`=EMPLOYEE) (required) |
| `customernum` | text | Customer number; only for PROSPECT, CUSTOMER, CUSTOMERANDSUPPLIER, or EMPLOYEE (required) |
| `suppliernum` | text | Supplier number; only for SUPPLIER or CUSTOMERANDSUPPLIER (required) |
| `taxid` | text | Tax ID (e.g. VATIN or SSN) (required) |
| `currency` | varchar | Currency code (ISO 4217) (required) |
| `locked` | integer | Deny booking of billing or procurement transactions (required) |
| `excludetax` | integer | Exclude from taxation (required) |
| `description` | text | Detailed general description (required) |

**GIN-indexed fields** (use `filters` plural): `assigneduser`, `customernum`, `firstname`, `lastname`, `suppliernum`

---

### Contacts

| Field | Type | Description |
|-------|------|-------------|
| `ID` | integer | Contact ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `assigneduser` | integer | Assigned user ID |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `davserver` | integer | DAV server ID |
| `picbinfile` | integer | Binary file ID; read-only (not for PUT or PATCH) |
| `visibility` | integer | Visibility (`0`=REGULAR, `1`=ARCHIVED, `2`=DELETED) (required) |
| `lastname` | text | Last name (surname or company name); required if `firstname` is empty (required) |
| `firstname` | text | First name (given name); required if `lastname` is empty (required) |
| `type` | integer | Contact type (`0`=COMPANY, `1`=PERSON) (required) |
| `title` | text | Title or salutation; only for PERSON (required) |
| `company` | text | Company name; only for PERSON (required) |
| `position` | text | Position or job title; only for PERSON (required) |
| `department` | text | Department; only for PERSON (required) |
| `address` | text | Address (street and building/suite number) (required) |
| `postalcode` | text | Postal or ZIP code (required) |
| `city` | text | City or locality (required) |
| `region` | text | Region or state (required) |
| `country` | varchar | Country code (ISO 3166-1 alpha-2) (required) |
| `phone` | text | Primary phone number (required) |
| `phone2` | text | Secondary phone number (required) |
| `cell` | text | Cell phone number (required) |
| `fax` | text | Fax number (required) |
| `email` | text | Primary e-mail address (required) |
| `email2` | text | Secondary e-mail address (required) |
| `website` | text | Website URL (required) |
| `birthdate` | timestamp | Birth date as Unix timestamp; only for PERSON |
| `description` | text | Detailed general description (required) |

**GIN-indexed fields** (use `filters` plural): `assigneduser`, `davserver`, `company`, `email`, `email2`, `firstname`, `lastname`

---

### Tickets

| Field | Type | Description |
|-------|------|-------------|
| `ID` | integer | Ticket ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `assigneduser` | integer | Assigned user ID |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `account` | integer | Account ID; mutually exclusive with `project` |
| `project` | integer | Project ID; mutually exclusive with `account` |
| `visibility` | integer | Visibility (`0`=REGULAR, `1`=ARCHIVED, `2`=DELETED) (required) |
| `name` | text | Name (required) |
| `ticketnum` | text | Ticket number (required) |
| `date` | timestamp | Designated date as Unix timestamp (defaults to now on creation) (required) |
| `duedate` | timestamp | Due date as Unix timestamp |
| `status` | integer | Status (`0`=NOTSTARTED, `1`=AWAITINGACCEPTANCE, `2`=ACCEPTED, `3`=REJECTED, `4`=ACTIVE, `5`=INACTIVE, `6`=FEEDBACKREQUIRED, `7`=TESTING, `8`=CANCELLED, `9`=COMPLETED, `10`=FAILED, `11`=BOOKED) (required) |
| `priority` | integer | Priority (`0`=LOWEST, `1`=LOW, `2`=MEDIUM, `3`=HIGH, `4`=HIGHEST) (required) |
| `description` | text | Detailed general description (required) |
| `billingitems` | json | JSON-encoded billing items (array) |
| `procurementitems` | json | JSON-encoded procurement items (array) |

**GIN-indexed fields** (use `filters` plural): `assigneduser`, `project`, `name`, `ticketnum`

---

### Tasks

| Field | Type | Description |
|-------|------|-------------|
| `ID` | integer | Task ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `assigneduser` | integer | Assigned user ID |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `davserver` | integer | DAV server ID |
| `ticket` | integer | Ticket ID; mutually exclusive with `project` |
| `project` | integer | Project ID; mutually exclusive with `ticket` |
| `visibility` | integer | Visibility (`0`=REGULAR, `1`=ARCHIVED, `2`=DELETED) (required) |
| `name` | text | Name (required) |
| `tasknum` | text | Task number (required) |
| `datefrom` | timestamp | Start date as Unix timestamp |
| `duedate` | timestamp | Due date as Unix timestamp |
| `status` | integer | Status (`0`=NOTSTARTED, `1`=AWAITINGACCEPTANCE, `2`=ACCEPTED, `3`=REJECTED, `4`=ACTIVE, `5`=INACTIVE, `6`=FEEDBACKREQUIRED, `7`=TESTING, `8`=CANCELLED, `9`=COMPLETED, `10`=FAILED, `11`=BOOKED) (required) |
| `priority` | integer | Priority (`0`=LOWEST, `1`=LOW, `2`=MEDIUM, `3`=HIGH, `4`=HIGHEST) (required) |
| `projectedeffort` | integer | Projected effort in minutes (required) |
| `description` | text | Detailed general description (required) |

**GIN-indexed fields** (use `filters` plural): `assigneduser`, `davserver`, `project`, `name`, `tasknum`

---

### Projects

| Field | Type | Description |
|-------|------|-------------|
| `ID` | integer | Project ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `assigneduser` | integer | Assigned user ID |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `account` | integer | Account ID |
| `visibility` | integer | Visibility (`0`=REGULAR, `1`=ARCHIVED, `2`=DELETED) (required) |
| `name` | text | Name (required) |
| `projectnum` | text | Project number (required) |
| `status` | integer | Status (`0`=DRAFT, `1`=NOTSTARTED, `2`=AWAITINGAPPROVAL, `3`=APPROVED, `4`=DISMISSED, `5`=ACTIVE, `6`=INACTIVE, `7`=TESTING, `8`=CANCELLED, `9`=COMPLETED, `10`=FAILED, `11`=BOOKED) (required) |
| `description` | text | Detailed general description (required) |

**GIN-indexed fields** (use `filters` plural): `assigneduser`, `name`, `projectnum`

---

### Appointments

| Field | Type | Description |
|-------|------|-------------|
| `ID` | integer | Appointment ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `assigneduser` | integer | Assigned user ID |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `davserver` | integer | DAV server ID |
| `visibility` | integer | Visibility (`0`=REGULAR, `1`=ARCHIVED, `2`=DELETED) (required) |
| `name` | text | Name (required) |
| `location` | text | Location (required) |
| `color` | varchar | Color code (CSS hex without `#`) (required) |
| `datefrom` | timestamp | Start date as Unix timestamp; must be <= `dateto` (required) |
| `dateto` | timestamp | End date as Unix timestamp; must be >= `datefrom` (required) |
| `recurrence` | integer | Recurrence (`0`=DAY, `1`=WORKDAY, `2`=WEEK, `3`=MONTH, `4`=YEAR) |
| `interval` | integer | Recurrence interval in minutes (required) |
| `maxoccurrences` | integer | Maximum occurrences including start date (`0`=unlimited) (required) |
| `daterecurrence` | timestamp | Recurrence end date as Unix timestamp |
| `description` | text | Detailed general description (required) |

**GIN-indexed fields** (use `filters` plural): `assigneduser`, `davserver`, `location`, `name`

---

### Transactions

| Field | Type | Description |
|-------|------|-------------|
| `ID` | integer | Transaction ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `assigneduser` | integer | Assigned user ID |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `account` | integer | Account ID |
| `item` | integer | Item ID; must be `null` for BILLING and PROCUREMENT |
| `contract` | integer | Contract ID |
| `transactionnum` | text | Transaction number (required) |
| `type` | integer | Transaction type (`0`=BILLING_QUOTE, `1`=BILLING_ORDER, `2`=BILLING_DELIVERY, `3`=BILLING_INVOICE, `4`=BILLING_CREDIT, `5`=PROCUREMENT_REQUEST, `6`=PROCUREMENT_ORDER, `7`=PROCUREMENT_DELIVERY, `8`=PROCUREMENT_INVOICE, `9`=PROCUREMENT_CREDIT, `10`=PRODUCTION_FABRICATION, `11`=PRODUCTION_DISASSEMBLY) (required) |
| `date` | timestamp | Designated date as Unix timestamp (defaults to now on creation) (required) |
| `duedate` | timestamp | Due date as Unix timestamp |
| `status` | integer | Status (`0`=DRAFT, `1`=BOOKED, `2`=HOLD, `3`=CANCELLED, `4`=CLOSED, ... `20`=PAID, `21`=OVERPAID, `22`=PROCESSED, `23`=PROCESSED_CANCELLED) (required) |
| `calculation` | integer | Calculation method (`0`=NET, `1`=GROSS, `2`=EXACT, `3`=LEGACY, `4`=EXTERNAL) (required) |
| `productionfactor` | integer | Production factor; required for PRODUCTION, otherwise `null` |
| `currency` | varchar | Currency code (ISO 4217) (required) |
| `exchangerate` | float | Exchange rate as multiple of system currency unit (required) |
| `taxid` | text | Buyer Tax ID (e.g. VATIN or SSN) (required) |
| `shippingrecipient` | text | Shipping recipient (required) |
| `shippingaddress` | text | Shipping address (required) |
| `shippingpostalcode` | text | Shipping postal code (required) |
| `shippingcity` | text | Shipping city (required) |
| `shippingregion` | text | Shipping region or state (required) |
| `shippingcountry` | varchar | Shipping country code (ISO 3166-1 alpha-2) (required) |
| `billingrecipient` | text | Billing recipient (required) |
| `billingaddress` | text | Billing address (required) |
| `billingpostalcode` | text | Billing postal code (required) |
| `billingcity` | text | Billing city (required) |
| `billingregion` | text | Billing region or state (required) |
| `billingcountry` | varchar | Billing country code (ISO 3166-1 alpha-2) (required) |
| `sellertaxid` | text | Seller Tax ID (required) |
| `sellername` | text | Seller name (required) |
| `selleraddress` | text | Seller address (required) |
| `sellerpostalcode` | text | Seller postal code (required) |
| `sellercity` | text | Seller city (required) |
| `sellerregion` | text | Seller region or state (required) |
| `sellercountry` | varchar | Seller country code (ISO 3166-1 alpha-2) (required) |
| `discount` | float | Total absolute discount (required) |
| `netamount` | float | Total net amount (required) |
| `tax` | float | Total tax amount (required) |
| `margin` | float | Total absolute margin (required) |
| `weight` | float | Total shipping weight in kg (required) |
| `items` | json | JSON-encoded line items (array) |

**GIN-indexed fields** (use `filters` plural): `assigneduser`, `transactionnum`

---

### Items

| Field | Type | Description |
|-------|------|-------------|
| `ID` | integer | Item ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `model` | integer | Model item ID; only for non-MODEL items |
| `picbinfile` | integer | Binary file ID; read-only (not for PUT or PATCH) |
| `visibility` | integer | Visibility (`0`=REGULAR, `1`=ARCHIVED, `2`=DELETED) (required) |
| `name` | text | Product name (required) |
| `manufacturer` | text | Manufacturer (brand or company) (required) |
| `itemnum` | text | Item number / SKU (required) |
| `barcode` | text | Barcode (e.g. GTIN, EAN, UPC) (required) |
| `type` | integer | Item type (`0`=SIMPLE, `1`=SERIALS, `2`=CHARGES, `3`=SERIALSANDCHARGES, `4`=SET, `5`=CONTAINER, `6`=NOSTOCK, `7`=MODEL) (required) |
| `forcestock` | integer | Force stock check on depletion (`0`=STORAGE, `1`=LOCATION) |
| `applicability` | integer | Applicability (`0`=ALWAYS, `1`=NEVER, `2`=BILLINGONLY, `3`=PROCUREMENTONLY) (required) |
| `unit` | varchar | Unit code (UN/CEFACT Recommendation 20) (required) |
| `sellingprice` | float | Default selling price per unit (required) |
| `purchaseprice` | float | Default purchase price per unit (required) |
| `taxrate` | float | Tax rate in percent |
| `weight` | float | Shipping weight per unit in kg (required) |
| `classcode` | text | Product classification code (e.g. GPC, UNSPSC) (required) |
| `tariffcode` | text | Tariff code (e.g. HS, CN, HTS) (required) |
| `origin` | varchar | Origin country code (ISO 3166-1 alpha-2) (required) |
| `description` | text | Detailed general description (required) |
| `foreigntaxrates` | json | Country-specific tax rates; use country code as key |

**GIN-indexed fields** (use `filters` plural): `barcode`, `itemnum`, `manufacturer`, `name`

---

### Opportunities

| Field | Type | Description |
|-------|------|-------------|
| `ID` | integer | Opportunity ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `assigneduser` | integer | Assigned user ID |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `account` | integer | Account ID |
| `contact` | integer | Contact ID |
| `campaign` | integer | Campaign ID |
| `visibility` | integer | Visibility (`0`=REGULAR, `1`=ARCHIVED, `2`=DELETED) (required) |
| `name` | text | Name (required) |
| `opportunitynum` | text | Opportunity number (required) |
| `date` | timestamp | Designated date as Unix timestamp (defaults to now on creation) (required) |
| `duedate` | timestamp | Due date as Unix timestamp |
| `status` | integer | Status (`0`=UNEVALUATED, `1`=ELIGIBLE, `2`=FEEDBACKREQUIRED, `3`=INNEGOTIATION, `4`=OFFERED, `5`=ACCEPTED, `6`=REJECTED) (required) |
| `priority` | integer | Priority (`0`=LOWEST, `1`=LOW, `2`=MEDIUM, `3`=HIGH, `4`=HIGHEST) (required) |
| `probability` | integer | Probability of success in percent; must be `100` for ACCEPTED (required) |
| `worstcase` | float | Worst-case monetary outcome (required) |
| `mostlikely` | float | Most likely monetary outcome (required) |
| `upside` | float | Upside monetary outcome (required) |
| `description` | text | Detailed general description (required) |

**GIN-indexed fields** (use `filters` plural): `assigneduser`, `name`, `opportunitynum`

---

### Documents

| Field | Type | Description |
|-------|------|-------------|
| `ID` | integer | Document ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `assigneduser` | integer | Assigned user ID |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `binfile` | integer | Binary file ID; read-only (not for PUT or PATCH) |
| `visibility` | integer | Visibility (`0`=REGULAR, `1`=ARCHIVED, `2`=DELETED) (required) |
| `name` | text | Name (required) |
| `documentnum` | text | Document number (required) |
| `status` | integer | Status (`0`=DRAFT, `1`=FEEDBACKREQUIRED, `2`=INREVISION, `3`=AWAITINGAPPROVAL, `4`=FINAL, `5`=OBSOLETE) (required) |
| `filename` | text | Filename (required) |
| `mimetype` | text | MIME type (required) |
| `public` | integer | Publicly accessible (required) |
| `description` | text | Detailed general description (required) |

**GIN-indexed fields** (use `filters` plural): `assigneduser`, `documentnum`, `filename`, `name`

---

### Notes

| Field | Type | Description |
|-------|------|-------------|
| `ID` | integer | Note ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `assigneduser` | integer | Assigned user ID |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `binfile` | integer | Binary file ID; read-only (not for PUT or PATCH) |
| `visibility` | integer | Visibility (`0`=REGULAR, `1`=ARCHIVED, `2`=DELETED) (required) |
| `name` | text | Name (required) |
| `status` | integer | Status (`0`=DRAFT, `1`=FEEDBACKREQUIRED, `2`=INREVISION, `3`=AWAITINGAPPROVAL, `4`=FINAL, `5`=OBSOLETE) (required) |
| `contenttype` | text | Content MIME type (required) |
| `text` | text | Plain text content (required) |
| `attachments` | text[] | Array of attachment filenames |
| `description` | text | Detailed general description (required) |

**GIN-indexed fields** (use `filters` plural): `assigneduser`, `name`

---

### Messages

| Field | Type | Description |
|-------|------|-------------|
| `ID` | integer | Message ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `mailserver` | integer | Mail server ID |
| `ticket` | integer | Ticket ID; mutually exclusive with `opportunity` |
| `opportunity` | integer | Opportunity ID; mutually exclusive with `ticket` |
| `mailinglist` | integer | Mailing list ID |
| `reference` | integer | Reference message (reply-to) ID; must be distinct from `ID` |
| `binfile` | integer | Binary file ID; read-only (not for PUT or PATCH) |
| `mailbox` | integer | Mailbox type (`0`=INBOX, `1`=DRAFTS, `2`=SENT, `3`=TEMPLATES, `4`=MAILINGS, `5`=ARCHIVE, `6`=TRASH, `7`=JUNK) (required) |
| `verified` | integer | Verified sender e-mail address (required) |
| `date` | timestamp | Designated date as Unix timestamp (defaults to now on creation) (required) |
| `subject` | text | Subject (required) |
| `sender` | text | Sender name and e-mail address (required) |
| `sender_email` | text | Sender e-mail address (required) |
| `sender_name` | text | Sender name (required) |
| `to` | text | All regular recipient names and e-mail addresses (required) |
| `to_email` | text | First regular recipient e-mail address (required) |
| `to_name` | text | First regular recipient name (required) |
| `to_count` | integer | Number of regular recipients (required) |
| `cc` | text | Carbon copy recipients (required) |
| `bcc` | text | Blind carbon copy recipients (required) |
| `contenttype` | text | Content MIME type (required) |
| `text` | text | Plain text content (required) |
| `attachments` | text[] | Array of attachment filenames |
| `senddate` | timestamp | Scheduled send date as Unix timestamp |
| `senderror` | text | Last send error message (required) |
| `messageid` | text | Message-ID header (required) |

**GIN-indexed fields** (use `filters` plural): `mailinglist`, `mailserver`, `sender`, `subject`, `to`

---

### Comments

| Field | Type | Description |
|-------|------|-------------|
| `ID` | timestamp | Comment ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `record` | timestamp | Record ID (parent record dependency) (required) |
| `date` | timestamp | Designated date as Unix timestamp (defaults to now on creation) (required) |
| `sender` | text | Sender (required) |
| `text` | text | Comment text (Markdown for rich text) (required) |
| `meta` | json | JSON-encoded metadata (object) |

---

### Events

| Field | Type | Description |
|-------|------|-------------|
| `ID` | integer | Event ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `entity` | t_entity | Canonical entity (required) |
| `index` | integer | Entity ID (required) |
| `name` | text | Name (required) |
| `color` | varchar | Color code (CSS hex without `#`) (required) |
| `datefrom` | timestamp | Start date as Unix timestamp; must be <= `dateto` (required) |
| `dateto` | timestamp | End date as Unix timestamp; must be >= `datefrom` (required) |
| `meta` | json | JSON-encoded metadata (object) |

---

### Campaigns

| Field | Type | Description |
|-------|------|-------------|
| `ID` | integer | Campaign ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `assigneduser` | integer | Assigned user ID |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `visibility` | integer | Visibility (`0`=REGULAR, `1`=ARCHIVED, `2`=DELETED) (required) |
| `name` | text | Name (required) |
| `datefrom` | timestamp | Start date as Unix timestamp; must be <= `dateto` (required) |
| `dateto` | timestamp | End date as Unix timestamp; must be >= `datefrom` |
| `status` | integer | Status (`0`=DRAFT, `1`=NOTSTARTED, `2`=AWAITINGAPPROVAL, `3`=APPROVED, `4`=DISMISSED, `5`=ACTIVE, `6`=INACTIVE, `7`=INEVALUATION, `8`=CANCELLED, `9`=CLOSED) (required) |
| `description` | text | Detailed general description (required) |

**GIN-indexed fields** (use `filters` plural): `assigneduser`, `name`

---

### Contracts

| Field | Type | Description |
|-------|------|-------------|
| `ID` | integer | Contract ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `assigneduser` | integer | Assigned user ID |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `account` | integer | Account ID |
| `visibility` | integer | Visibility (`0`=REGULAR, `1`=ARCHIVED, `2`=DELETED) (required) |
| `name` | text | Name (required) |
| `contractnum` | text | Contract number (required) |
| `date` | timestamp | Designated date as Unix timestamp (defaults to now on creation) (required) |
| `datefrom` | timestamp | Start date as Unix timestamp; must be <= `dateto` |
| `dateto` | timestamp | End date as Unix timestamp; must be >= `datefrom` |
| `datecancel` | timestamp | Cancellation date as Unix timestamp |
| `status` | integer | Status (`0`=DRAFT, `1`=AWAITINGAPPROVAL, `2`=APPROVED, `3`=DISMISSED, `4`=ACTIVE, `5`=INACTIVE, `6`=EXPIRED, `7`=CANCELLED, `8`=CLOSED) (required) |
| `currency` | varchar | Currency code (ISO 4217) (required) |
| `exchangerate` | float | Exchange rate as multiple of system currency unit (required) |
| `billingcycle` | integer | Billing cycle in months |
| `lastbilling` | timestamp | Last billing date as Unix timestamp |
| `description` | text | Detailed general description (required) |
| `contractitems` | json | JSON-encoded contract items (array) |
| `billingitems` | json | JSON-encoded billing items (array) |
| `procurementitems` | json | JSON-encoded procurement items (array) |
| `autobilling` | json | JSON-encoded auto-billing data; only if `billingcycle` is set |

**GIN-indexed fields** (use `filters` plural): `assigneduser`, `status`, `contractnum`, `name`

---

### Files

| Field | Type | Description |
|-------|------|-------------|
| `ID` | timestamp | File ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `record` | timestamp | Record ID (parent dependency); mutually exclusive with `comment` |
| `comment` | timestamp | Comment ID (parent dependency); mutually exclusive with `record` |
| `binfile` | integer | Binary file ID; read-only (not for PUT or PATCH) |
| `filename` | text | Filename (required) |
| `mimetype` | text | MIME type (required) |

**GIN-indexed fields** (use `filters` plural): `filename`

---

### Payments

| Field | Type | Description |
|-------|------|-------------|
| `ID` | integer | Payment ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `assigneduser` | integer | Assigned user ID |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `ledger` | integer | Ledger ID |
| `transaction` | integer | Transaction ID; mutually exclusive with `account` |
| `account` | integer | Account ID; mutually exclusive with `transaction` |
| `date` | timestamp | Designated date as Unix timestamp (defaults to now on creation) (required) |
| `subject` | text | Subject (e.g. bank statement or reference number) (required) |
| `status` | integer | Status (`0`=DRAFT, `1`=COMPLETED, `2`=CANCELLED, `3`=BOOKED) (required) |
| `amount` | float | Amount (monetary) (required) |
| `autoadvance` | integer | Auto-advance to next transaction (required) |
| `description` | text | Detailed general description (required) |

**GIN-indexed fields** (use `filters` plural): `assigneduser`, `ledger`, `subject`

---

### Users

| Field | Type | Description |
|-------|------|-------------|
| `ID` | integer | User ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `contact` | integer | Contact ID |
| `activity` | integer | Activity (`0`=ACTIVE, `1`=DEACTIVATED, `2`=DELETED) (required) |
| `name` | text | Username (case-insensitively unique) (required) |
| `email` | text | System e-mail address (case-insensitively unique) (required) |
| `nopublic` | integer | Deny access to public data (required) |
| `apionly` | integer | Restricted to API access, no regular login (required) |
| `expdate` | timestamp | Expiry date as Unix timestamp |
| `description` | text | Description (required) |

**GIN-indexed fields** (use `filters` plural): `email`, `name`

---

### Groups

| Field | Type | Description |
|-------|------|-------------|
| `ID` | integer | Group ID (required) |
| `creator` | integer | Creator user ID (defaults to authenticated user on creation) |
| `creationdate` | timestamp | Creation date as Unix timestamp (defaults to now on creation) (required) |
| `lastmodified` | timestamp | Last modification date as Unix timestamp (auto-reset on modification) (required) |
| `leader` | integer | Leader user ID |
| `activity` | integer | Activity (`0`=ACTIVE, `1`=DEACTIVATED, `2`=DELETED) (required) |
| `name` | text | Name (case-insensitively unique) (required) |
| `description` | text | Description (required) |

**GIN-indexed fields** (use `filters` plural): `name`

---

Canonical: https://www.zeyos.com/guide/cli/commands

# Commands Reference

Complete reference for every CLI command, with options and examples.

## Global Options

These options work with any command:

| Option | Description |
|--------|-------------|
| `--json` | Output as formatted JSON |
| `--yaml` | Output as YAML |
| `--profile <name>` | Use a named credential profile for this command |
| `--dry-run` | Print the request route and body without sending it |
| `--timeout <seconds>` | Per-request timeout (default 30) |
| `--no-validate` | Skip schema validation of fields and filters |
| `--no-color` | Disable ANSI color output |
| `-h`, `--help` | Show help for a command |
| `-v`, `--version` | Print the CLI version |

This table is maintained by hand. For the authoritative, always-current list — including which
flags each individual command accepts — run `zeyos commands --json`.

Global options may be placed before or after the command name:

```bash
zeyos --profile dev whoami
zeyos whoami --profile dev
```

---

## login

Authenticate with a ZeyOS instance via OAuth 2.0 authorization code flow.

```
zeyos login [options]
```

| Option | Description |
|--------|-------------|
| `--base-url <url>` | ZeyOS platform URL |
| `--client-id <id>` | OAuth application ID |
| `--secret <secret>` | OAuth application secret |
| `--scope <scope>` | OAuth scope |
| `--port <port>` | Local callback port (default: `9005`) |
| `--global` | Save credentials to global config |
| `--force` | Overwrite existing credentials |
| `--clean` | Discard saved config and re-prompt for everything |
| `--manual` | Don't open browser; paste code manually |

**Examples:**

```bash
# Interactive login (prompts for missing values)
zeyos login

# Pre-fill connection values; the OAuth browser/code step still runs
zeyos login --base-url https://cloud.zeyos.com/demo \
            --client-id myapp --secret "$ZEYOS_CLIENT_SECRET"

# Start fresh, ignore any saved credentials
zeyos login --clean

# Manual mode (useful in SSH / headless environments)
zeyos login --manual
```

:::info
When values are not provided as flags, the CLI prompts interactively for the ZeyOS URL, application ID, and application secret. The secret prompt does not echo input. For CI or fully unattended agents, provide `ZEYOS_BASE_URL`, `ZEYOS_TOKEN`, and optionally `ZEYOS_REFRESH_TOKEN`, `ZEYOS_CLIENT_ID`, and `ZEYOS_CLIENT_SECRET` through the environment instead of running `zeyos login`.
:::

---

## logout

Revoke the stored token and clear saved credentials.

```
zeyos logout [--global]
```

| Option | Description |
|--------|-------------|
| `--global` | Clear global credentials instead of local |

**Examples:**

```bash
zeyos logout          # Clear local .zeyos/auth.json credentials
zeyos logout --global # Clear ~/.config/zeyos/credentials.json tokens
```

---

## whoami

Show information about the currently authenticated user.

```
zeyos whoami [--json|--yaml]
```

**Examples:**

```bash
zeyos whoami          # Table output
zeyos whoami --json
zeyos whoami --show-token --json   # explicitly include the current access token
```

If the stored refresh token is invalid or expired, interactive text-mode `whoami`
prints the credential source and asks whether to re-authenticate immediately. In
`--json`, `--yaml`, or non-interactive runs, it exits with the same diagnostic and
prints the matching `zeyos login --force` command instead of prompting.

---

## profile

Manage named credential profiles and switch between ZeyOS instances. See [Configuration → Profiles](https://www.zeyos.com/guide/cli/configuration#profiles) for the full model.

```
zeyos profile <list|current|use|add|remove> [options]
```

| Command | Description |
|---------|-------------|
| `profile list` | List all profiles; the active one is marked `*`, with token status |
| `profile current` | Show which profile resolves right now, and why (flag/env/pin/active) |
| `profile use <name>` | Make `<name>` the active profile (global) |
| `profile use <name> --local` | Pin `<name>` to the current project (`.zeyos/profile`) |
| `profile add [<name>] [opts]` | Create/update a profile; prompts for missing values when run without options |
| `profile remove <name>` | Delete a profile |

**Examples:**

```bash
zeyos profile add                         # prompt for name, URL, app ID, secret
zeyos profile add dev  --base-url https://zeyos.example.com/dev
zeyos profile add prod --from-current        # snapshot current credentials
zeyos login --profile prod                   # authenticate into & activate a profile
zeyos profile use dev                         # switch active profile
zeyos profile use prod --local                # pin to this project
zeyos list tickets --profile dev              # one-off override on any command
```

---

## list

Query and list records for a resource with filtering, sorting, and pagination.

```
zeyos list <entity> [options]
```

Run `zeyos list` with no entity to see everything you can query, grouped by business area:

```bash
zeyos list            # grouped overview
zeyos resources       # the same overview
zeyos resources --json  # flat array, with group / description / transactionType
```

| Option | Description |
|--------|-------------|
| `--fields <fields>` | Field selection — comma-separated, JSON object, or JSON array (see below) |
| `--filter <json>` | Filter criteria — JSON object |
| `--filter-file <path>` | Read filter criteria from a JSON file |
| `--search <text>` | Full-text search (sent as the API `query` parameter) |
| `--preset <name>` | Apply a resource business preset; an explicit `--filter` is merged on top |
| `--sort <fields>` | Sort fields, comma-separated (prefix `+` asc, `-` desc) |
| `--limit <n>` | Maximum records to return (default: `50`) |
| `--offset <n>` | Skip the first n records |
| `--expand <fields>` | Expand JSON/binary columns (e.g. binfile, items) |
| `--extdata` | Include extended data fields |
| `--json` | JSON output |
| `--yaml` | YAML output |
| `--dry-run` | Print the request without sending it |
| `--no-validate` | Skip the CLI's default schema validation |

**Fields format:**

The `--fields` option supports three formats:

| Format | Example |
|--------|---------|
| Comma-separated | `--fields ID,name,status` |
| JSON object (with aliases) | `--fields '{"Name":"lastname","City":"contact.city"}'` |
| JSON array | `--fields '["ID","name","status"]'` |

**Filter compatibility:**

The CLI normalizes common agent- and ORM-generated filter forms before sending the request:

- arrays become `IN` — `{"status":[1,3]}`
- Mongo style — `$lt $lte $gt $gte $ne $in $nin $like $ilike $regex $eq $between`
- suffix keys — `lastname__startswith`, `lastname__endswith`, `lastname__contains`, `lastname__like`, `ID__gt`, `status__in`, `status__nin`, `duedate__between`
- the single-underscore form where unambiguous — `status_neq`, `ID_gt` (a field that genuinely contains an underscore, like `sender_email`, is never split)
- **composite** — `{"$or":[…]}` and `{"$and":[…]}` become ZeyOS numbered logical groups (`{"0":["OR",…]}`)

On `accounts`, `name` in filters or `--fields` resolves to `lastname`.

`startswith` / `endswith` / `contains` take **literal** text, so `%` and `_` in your value are escaped. `like` / `ilike` take a **pattern** where `%` is a wildcard.

An operator the CLI cannot translate is rejected before the request with the supported set in the error (exit `2`), rather than being forwarded for the server to refuse opaquely. ZeyOS has no IS NULL filter, so `field__isnull` is refused with the client-side alternative.

Run `zeyos describe <entity>` for the authoritative operator list — it is printed in a `filter operators` section, and carried under `filterOperators` in `--json`.

Before sending, data commands validate filter, selected, sorted, and written fields against
the generated schema. Unknown fields fail fast with a suggestion and the valid-field list;
every join hop and terminal field must exist. Individual `extdata.*` keys require the
[instance-specific snapshot](https://www.zeyos.com/guide/cli/configuration#custom-field-validation); missing
metadata fails explicitly. Use `--no-validate` only for a separately reviewed unsupported
contract, never merely to silence a guessed field. If a filtered or searched list is empty, the CLI prints a stderr hint pointing
to `describe` and `find`, including in JSON and YAML modes.

**Transaction entities:**

`transactions` is a single table holding twelve different business documents, separated only by the `type` column. Each type is exposed as its own entity, so you never filter on `type` by hand:

| Entity | `transactions.type` | Meaning |
|--------|--------------------|---------|
| `billing_quotes` | 0 | Quotes issued to customers |
| `billing_orders` | 1 | Sales orders |
| `billing_deliveries` | 2 | Delivery notes to customers |
| `billing_invoices` | 3 | Invoices issued to customers |
| `billing_credits` | 4 | Credit notes issued to customers |
| `procurement_requests` | 5 | Purchase requisitions |
| `procurement_orders` | 6 | Purchase orders to suppliers |
| `procurement_deliveries` | 7 | Goods received from suppliers |
| `procurement_invoices` | 8 | Supplier invoices (bills) |
| `procurement_credits` | 9 | Credit notes from suppliers |
| `production_fabrications` | 10 | Production/assembly runs |
| `production_disassemblies` | 11 | Disassembly runs |

Singular, plural and hyphenated spellings all resolve (`billing_invoice`, `billing_invoices`, `billing-invoices`). Unqualified sales vocabulary maps to the billing side — `invoices`, `orders`, `quotes`, `credits`, `deliveries` — while `bills`, `po` and `purchase_orders` map to procurement. Use `transactions` to query every type at once.

The type binding is applied beneath any `--preset` and any `--filter`, and is set automatically on `create`:

```bash
zeyos list billing_invoices --preset overdue
zeyos create billing_invoices --account 42 --currency EUR   # sends type: 3
```

Invoice and credit entities accept these presets:

| Preset | Filter |
|--------|--------|
| `open` | Excludes cancelled, closed, paid, overpaid and processed status variants |
| `overdue` | Open, with `duedate` before now |
| `paid` | Status `20` or `21` |
| `draft` / `booked` / `cancelled` | Status `0` / `1` / `3` |

The older presets on the `transactions` entity itself (`quotes`, `orders`, `invoices`, `credits`, `open-invoices`, `overdue-invoices`, `paid-invoices`) still work.

**Examples:**

```bash
# List tickets with default configured fields
zeyos list tickets

# Custom filters
zeyos list tickets --filter '{"status":1,"priority":3}'

# Custom filters from a file
zeyos list tickets --filter-file ./filters/open-tickets.json

# Specify fields with aliases
zeyos list accounts --fields '{"Name":"lastname","City":"contact.city"}'

# Comma-separated fields
zeyos list tickets --fields ID,name,status,priority

# Sort by multiple columns
zeyos list tickets --sort "+name,-lastmodified"

# Pagination
zeyos list tickets --limit 10 --offset 20

# Include extended data
zeyos list tickets --extdata

# JSON output for scripting
zeyos list tickets --json | jq length
```

:::tip Pagination Info
When results fill the page limit, the CLI makes a second API call to get the total count and displays:
```
Showing 1–10 of 47  (--offset 10 for next page)
```
:::

---

## find

Resolve a human label to records before filtering another resource by foreign-key ID.

```bash
zeyos find accounts "Zfx Lyon"
zeyos find projects "Website" --limit 5 --json
```

`find` sends the text through the API's full-text `query` parameter and returns ID plus the
resource's display fields. No matches is a successful result reported on stderr.

---

## count

Count records for a resource, with optional filtering. Returns a plain number by default.

```
zeyos count <resource> [options]
```

| Option | Description |
|--------|-------------|
| `--filter <json>` | Filter criteria — JSON object |
| `--filter-file <path>` | Read filter criteria from a JSON file |
| `--search <text>` | Full-text search |
| `--preset <name>` | Apply a resource business preset before the explicit filter |
| `--json` | Output as `{"count": N}` |
| `--yaml` | YAML output |
| `--dry-run` | Print the request without sending it |
| `--no-validate` | Skip schema validation |

**Examples:**

```bash
# Total tickets
zeyos count tickets
# → 47

# Filtered count
zeyos count tickets --filter '{"status":1}'
# → 12

# Filtered count using a JSON file
zeyos count tickets --filter-file ./filters/open-tickets.json

# JSON output for scripting
zeyos count accounts --json
# → {"count": 156}
```

---

## sum

Sum a numeric field across matching records. The CLI pages internally, so this is the
short path for simple totals that would otherwise require `list` plus a script.

```
zeyos sum <resource> <field> [options]
```

| Option | Description |
|--------|-------------|
| `--filter <json>` | Filter criteria — JSON object. Arrays normalize to `IN`, e.g. `{"status":[1,3]}` |
| `--filter-file <path>` | Read filter criteria from a JSON file |
| `--preset <name>` | Apply a resource business preset before the explicit filter |
| `--page-size <n>` | Records per API page (default: 50) |
| `--limit <n>` | Maximum records to inspect |
| `--offset <n>` | Initial offset |
| `--json` | Output as `{"sum": N, "count": N, "field": "..."}` |
| `--yaml` | YAML output |
| `--dry-run` | Print the first request without sending it |
| `--no-validate` | Skip schema validation |

**Examples:**

```bash
# Completed or booked effort minutes
zeyos sum actionsteps effort --filter '{"status":[1,3]}'

# Invoice net amount as JSON
zeyos sum transactions netamount --filter '{"type":3}' --json
```

---

## get

Fetch a single record by ID.

```
zeyos get <resource> <id> [options]
```

| Option | Description |
|--------|-------------|
| `--fields <fields>` | Field selection — comma-separated, JSON object, or JSON array |
| `--extdata` | Include extended data fields |
| `--tags` | Include tags |
| `--all` | Fetch all data (extdata + tags + all fields) |
| `--json` | JSON output |
| `--yaml` | YAML output |

**Aliases:** `show`

**Examples:**

```bash
# Get a ticket with configured fields
zeyos get ticket 42

# Include extended data
zeyos get ticket 42 --extdata

# Include tags
zeyos get ticket 42 --tags

# Include both extdata and tags
zeyos get ticket 42 --extdata --tags

# Get everything — all fields, extdata, and tags
zeyos get ticket 42 --all

# JSON output
zeyos get account 15 --json

# Using the alias
zeyos show ticket 42
```

:::info Date Formatting
Date fields like `duedate`, `lastmodified`, and `creationdate` are automatically formatted as `YYYY-MM-DD` in table/record output. Raw Unix timestamps are preserved in JSON and YAML output. The format is configurable via `dateFormat` in your auth config file.
:::

---

## create

Create a new record. Fields can be provided as a JSON blob or as individual flags.

```
zeyos create <resource> [--data <json>] [--field value ...]
```

| Option | Description |
|--------|-------------|
| `--data <json>` | Complete record as a JSON string |
| `--data-file <path>` | Read the complete record from a JSON file |
| `--<field> <value>` | Set individual field (any unknown flag becomes a field) |

**Examples:**

```bash
# Using --data JSON
zeyos create ticket --data '{"name":"Fix login bug","status":0,"priority":3}'

# Using a JSON file
zeyos create ticket --data-file ./ticket.json

# Using individual field flags
zeyos create ticket --name "Fix login bug" --status 0 --priority 3

# Create an account (accounts use --lastname, not --name)
zeyos create account --lastname "ACME Corp" --currency EUR --visibility 0

# JSON output (returns the created record)
zeyos create ticket --name "New feature" --json
```

:::info Type Coercion
Field values are coerced using the column's type from the schema, not from how the value looks:

- Numeric columns (`smallint`, `integer`, `numeric`, …) take numbers, so `--status 0` sends the integer `0`, not `"0"`.
- Boolean columns accept `true` / `false`.
- **Text columns keep their string value**, so `--customernum 00123` sends `"00123"` and `--phone "+4930123456"` keeps its `+`. Identifiers that merely look numeric are not damaged.
- `--<field> null` always sends JSON `null`.

For a field the schema doesn't know (a custom column, or with `--no-validate`), the value is converted only when it is a plain decimal number that round-trips exactly — a leading zero, a leading `+`, an exponent, hex, or a value beyond the safe integer range all stay text.

:::tip
To control types exactly, pass `--data` with explicit JSON: `--data '{"zip":"01067","amount":19.99}'`.
:::
:::

---

## update

Update an existing record by ID. Same input modes as `create`.

```
zeyos update <resource> <id> [--data <json>] [--field value ...]
```

**Aliases:** `edit`

**Examples:**

```bash
# Using --data JSON
zeyos update ticket 42 --data '{"status":4}'

# Using a JSON file
zeyos update ticket 42 --data-file ./ticket-update.json

# Using field flags
zeyos update ticket 42 --status 4 --priority 2

# Update account name (accounts use --lastname, not --name)
zeyos update account 15 --lastname "ACME Corporation"
```

---

## delete

Delete a record by ID. Prompts for confirmation by default.

```
zeyos delete <resource> <id> [--force]
```

| Option | Description |
|--------|-------------|
| `--force` | Skip confirmation prompt |

:::warning
`--force` is a flag and takes no value — `--force=false` is a usage error, not a way to turn it off. Omit the flag to keep the confirmation.

In a script or CI, stdin is usually closed, so the confirmation cannot be answered: the delete is **aborted** and the command exits `5`. Pass `--force` for non-interactive deletes.
:::

**Aliases:** `rm`, `remove`

**Examples:**

```bash
# Interactive confirmation
zeyos delete ticket 42

# Skip confirmation
zeyos delete ticket 42 --force

# Using aliases
zeyos rm ticket 42
zeyos remove ticket 42
```

---

## resources

List all curated CLI resources and their operations. This is the authoritative boundary for what the CLI supports directly.

```
zeyos resources
```

Shows a table of all CLI-supported resource types and available operations. Operational
workflows can use `actionstep` / `actionsteps` / `time-entries` for follow-ups and
effort records. Read-only platform schema definitions are available as `customfield` /
`customfields` with `list`, `get`, and therefore `count` support.

---

## describe

Show a resource's schema — fields, types, foreign keys and enum values — from the generated schema. Runs **offline** (no login required), so an agent can discover the data model before making any call.

```
# Field/type/enum/foreign-key listing for a resource
zeyos describe tickets

# Machine-readable schema
zeyos describe accounts --json
```

Foreign keys are shown as `→ <table>`, and enum fields list their valid values (e.g. `status` → `0=NOTSTARTED 1=AWAITINGACCEPTANCE …`). The operations available for the resource are listed at the bottom.

---

## doctor

Check local readiness for coding agents. This runs offline and never prints tokens or client secrets. It proves that configuration is in place, **not** that a server accepts it.

```bash
zeyos doctor agent
zeyos doctor agent --json
```

The report covers:

| Section | Checks | Affects `ok` and exit code |
|---------|--------|----------------------------|
| `cli`, `connection`, `auth`, `resources` | CLI version, base URL and instance, whether credentials are present and usable, and whether the resource registry loads | Yes |
| `customFields` | Whether `ZEYOS_CUSTOM_FIELDS_FILE` is set, absolute, readable and valid, which instance it is bound to compared with the configured one, and the file's age | Yes, when set and broken or mismatched, because that blocks every request. Unset is fine |
| `skills` | Installed skill bundles in the local and global skills directories of each agent | No, advisory |
| `project` | `zeyos.app.json` in the current directory, and the `zeysdk` version if the binary is installed | No, informational |
| `verification` | What was checked offline, and what was not checked live | Reports `offlineConfiguration.ok` |

`verification.liveServer.performed` is always `false`. Doctor does not check that the instance is reachable, that tokens are accepted, what the user may do, or whether a custom-field snapshot still matches the instance. Use `zeyos whoami` for a read-only authenticated check.

`customFields.status` is one of `not_set`, `ok`, `unbound` (no usable configured instance to compare), `relative_path`, `unreadable`, `invalid` or `instance_mismatch`. `ageDays` comes from the file's modification time, because a snapshot cannot record its own review date. It is a hint, not proof of freshness.

For skills, doctor reads the `.zeyos-skills.json` lockfile in each directory and hashes every tracked file. Each entry in `skills.roots` has one of these statuses:

| Status | Meaning |
|--------|---------|
| `ok` | Files match the lockfile, and the bundle matches the installed `@zeyos/client` |
| `outdated` | Files match, but the bundle version differs from the installed client |
| `drift` | Tracked files are missing, modified or cannot be verified. `files` gives the counts |
| `legacy` | ZeyOS skill folders exist with no lockfile, so they cannot be verified |
| `invalid` | The lockfile is unreadable, malformed, an unsupported version, or lists a path outside the skills root |
| `absent` | No ZeyOS skills here. Human output hides these |

Skills from other vendors in the same directory are ignored. Skill problems appear in `nextSteps` but do not fail the check, so a CI job that does not use skills is not blocked. The only `zeysdk` command doctor runs is `zeysdk version`.

---

## skills

Discover, install and verify the bundled ZeyOS agent skill packs for any coding agent (Claude Code, Codex, opencode, Factory Droid, pi, …). The `zeyos` skill is the router that classifies each task.

```
# List the bundled skills
zeyos skills list

# Print a skill's instructions
zeyos skills show zeyos-work-management

# Install — interactive: pick a coding agent, then local vs. global
zeyos skills install

# Install non-interactively with flags
zeyos skills install --target claude --global       # all projects
zeyos skills install --target opencode --local      # this project only
zeyos skills install zeyos-billing-insights -y      # one skill, defaults
zeyos skills install --dir ./vendor/skills          # any directory

# Preview an install or upgrade without writing anything
zeyos skills install --target claude --global --preview

# Compare an installed tree with its lockfile
zeyos skills verify --target claude --global
```

Run bare, `install` prints the ZeyOS banner and prompts for **(a)** which coding agent to target and **(b)** whether to install for this project or globally for every project. Pass `--target` and/or `--global`/`--local` to skip the matching prompt; pass `-y`/`--yes` (or pipe non-interactively) to skip all prompts and use flags plus sensible defaults.

Options for `install`:

| Option | Description |
|--------|-------------|
| `--target <agent>` | Coding agent: `claude`, `codex`, `opencode`, `droid`, `pi`, `agents` (prompted when omitted; otherwise auto-detected) |
| `--global` | Install into the agent's home directory (all projects) |
| `--local` | Install into the current project (default) |
| `--dir <path>` | Install into an explicit directory (overrides `--target`) |
| `--dry-run`, `--preview` | Print the plan and the bundle version change; write nothing |
| `--force` | Overwrite locally modified files and existing files missing from the lockfile. It never deletes a modified file, changes a symlink, or touches paths outside the bundle's own directories |
| `-y`, `--yes` | Skip prompts and use flags / sensible defaults |
| `--no-logo` | Don't print the ZeyOS banner |
| `--json` / `--yaml` | Print a machine-readable install summary (also silences the banner) |

Per-agent skill directories:

| Agent | `--local` | `--global` |
|-------|-----------|------------|
| `claude` | `.claude/skills/` | `~/.claude/skills/` |
| `codex` | `.codex/skills/` | `~/.codex/skills/` |
| `opencode` | `.opencode/skills/` | `~/.config/opencode/skills/` |
| `droid` | `.factory/skills/` | `~/.factory/skills/` |
| `pi` | `.pi/skills/` | `~/.pi/agent/skills/` |
| `agents` | `.agents/skills/` | `~/.agents/skills/` |

Skills are installed into `<dir>/<name>/` with everything they require. `install zeyos` includes `zeyos-dev` and all domain skills; a single domain skill brings `shared/` and the OKF bundle (`shared/okf/`). Links are rewritten so the tree needs no source checkout.

`<dir>/.zeyos-skills.json` records the bundle version, target, skills and a SHA-256 of each installed file. Installing again upgrades every skill it lists:

| Plan status | Meaning |
|-------------|---------|
| `added`, `updated`, `unchanged` | Normal install and upgrade results |
| `removed` | An unmodified tracked file the bundle no longer ships, inside its own directories |
| `locally-modified-kept` | Changed since installation; not overwritten |
| `locally-modified-overwritten-with-force` | Changed since installation; replaced by `--force`. A modified file the bundle dropped is kept, even with `--force` |
| `unknown-kept` | Differs from the bundle and is not in the lockfile, as after an older install; not overwritten |
| `unknown-overwritten-with-force` | The same, replaced by `--force` |
| `symlink-skipped` | The path or a directory above it is a symlink. It is reported and left untouched, even with `--force`. Remove the link to install there |
| `outside-bundle-kept` | The lockfile lists a path outside the bundle's own directories. It stays on disk and is no longer tracked |
| `conflict` | A file where a directory belongs, the reverse, or an unreadable file. The install stops before writing anything; `--preview` exits `1` |

Files you add yourself are never touched.

`zeyos skills verify` accepts `--dir`, `--target`, `--global` and `--local`, and never prompts:

| Exit | Meaning |
|------|---------|
| `0` | Every file is present. Modified files and symlinks are listed, and `ok` is then `false` in `--json` output |
| `1` | Files are missing, files from an older install were kept, or the lockfile is damaged or tracks nothing |
| `4` | No lockfile in that directory |

---

## okf

Work with the [Open Knowledge Format](https://www.zeyos.com/guide/okf/overview) bundle that ships with the
client — a portable Markdown description of the ZeyOS data model (one concept per
API-backed entity) plus curated metrics, playbooks, and query concepts.

```bash
zeyos okf list                  # list concepts (type, id, title); --json for automation
zeyos okf show tickets          # print a concept (bare resource name, or entities/tickets)
zeyos okf check                 # validate OKF v0.1 conformance (exit non-zero on error)
zeyos okf export --out ./okf    # copy the shipped bundle into a directory
zeyos okf build  --out ./okf    # synthesize a bundle from the client's schema
```

| Subcommand | What it does |
|-----------|--------------|
| `list` | List the concepts in the bundle (`--json`/`--yaml` supported). |
| `show <concept>` | Print one concept doc. Accepts a bare resource (`tickets`) or full id (`entities/tickets`). |
| `check` | Validate the bundle for OKF v0.1 conformance; exits non-zero on any error (CI-friendly). |
| `export` | Copy the shipped `okf/` bundle into `--out` (default `./okf`); `--force` to overwrite. |
| `build` | Synthesize a structural bundle from the client's schema into `--out` (default `./okf`). |

Options: `--dir <path>` reads from an explicit bundle directory (`list`/`show`/`check`);
`--out <path>` is the write target (`build`/`export`); `--force` overwrites an existing
target. `export` ships the rich curated bundle; `build` is the lighter runtime projection.

---

## Command Aliases

| Alias | Equivalent |
|-------|-----------|
| `show` | `get` |
| `edit` | `update` |
| `rm` | `delete` |
| `remove` | `delete` |
| `resource` | `resources` |
| `skill` | `skills` |

---

## Exit Codes

Every command reports its outcome through the exit code, so scripts and agents can
tell failure modes apart without parsing stderr.

| Code | Meaning | Typical cause |
|------|---------|---------------|
| `0` | Success | — |
| `1` | Runtime or API failure | The request reached the server and failed |
| `2` | Usage error | Unknown command, unknown flag, missing argument, a value passed to a boolean flag |
| `3` | Auth error | No credentials, an unknown `--profile`, HTTP 401/403 |
| `4` | Not found | The requested record does not exist |
| `5` | Aborted | A confirmation prompt was declined, or could not be answered because stdin was closed |

### Machine-readable failures

In `--json` or `--yaml` mode a failure writes a structured envelope to **stdout**, so a
command that is piped to `jq` still tells you what went wrong. The human message goes to
stderr as usual.

```bash
$ zeyos list billing_invoces --json
```
```json
{
  "ok": false,
  "error": {
    "code": "unknown_entity",
    "exitCode": 2,
    "message": "Unknown entity: \"billing_invoces\".  Did you mean \"billing_invoices\"?",
    "field": "billing_invoces",
    "suggestion": "billing_invoices",
    "actions": ["Run 'zeyos list' to see every entity."]
  }
}
```

Successful output is **unchanged** — a list is still a bare array. Tell the two apart by
shape: a failure is an object carrying `ok: false`.

`error.code` is a stable slug. Current values include `unknown_entity`, `unknown_command`,
`unknown_option`, `invalid_option_value`, `invalid_filter`, `unexpected_argument`,
`flag_takes_no_value`, `missing_argument`, `auth_required`, `auth_failed`, `not_found`,
`timeout`, `network`, and `api_<status>` for a server error.

```bash
zeyos get ticket 999999 --json
case $? in
  0) echo "found" ;;
  4) echo "no such ticket" ;;
  3) echo "run: zeyos login" ;;
  *) echo "failed" ;;
esac
```

:::note
`zeyos doctor agent` exits `3` when the local configuration is not ready, including a
broken or mismatched custom-field snapshot, which makes it usable directly as a CI
readiness check. It stays offline, so passing does not prove the server is reachable.
:::

---

## Timeouts

Requests time out after **30 seconds** by default. Override per command with
`--timeout <seconds>`, or globally with the `ZEYOS_TIMEOUT_MS` environment variable.

```bash
zeyos list transactions --preset open-invoices --timeout 120
```

Read operations (`list`, `count`, `get`, `find`, `sum`) are retried automatically on
timeouts and transient network errors. Writes are never retried on a timeout or a
`503`, so a create cannot be duplicated by a retry. A `429` is retried for reads and
writes, because it means the request was not processed.

---

## Number Formatting

Columns the schema types as floating point (`double precision`, `numeric`, `decimal`, `real`, `money`) are rendered with grouped thousands and two decimal places in table and record views, and right-aligned:

```
     ID  TRANSACTIONNUM        DATE  STATUS  ACCOUNT     NETAMOUNT
  ─────  ──────────────  ──────────  ──────  ───────  ────────────
  10932  N.2210.0654-01  2022-11-06       8     8840     17.009,00
  99999  R.2401.0001-01  2024-01-01       1     1234  1.234.567,89
```

Integer columns are deliberately left untouched — IDs, foreign keys, enum codes and Unix timestamps are all integers, and grouping them would be misleading.

The separators follow a locale, resolved in this order:

1. `ZEYOS_LOCALE` environment variable
2. `locale` in the resource config file
3. the host's default locale

```bash
ZEYOS_LOCALE=de-DE zeyos list billing_invoices   # 17.009,00
ZEYOS_LOCALE=en-US zeyos list billing_invoices   # 17,009.00
```

:::info
Formatting applies to human-readable output only. `--json` and `--yaml` always emit the raw
number, so scripts and agents are unaffected by the terminal's locale.
:::

---

## commands

Publish the command graph — every command, its aliases, and the flags it accepts — as data.

```
zeyos commands [--json|--yaml]
```

`zeyos describe` exposes the *data* model; `zeyos commands` exposes the *command* model, so
an agent can discover which flags exist without parsing prose help. Runs offline.

```bash
zeyos commands --json | jq '.commands[] | select(.name=="list") | .flags'
```

Each entry carries `name`, `aliases`, `flags` (global flags folded in), and
`acceptsArbitraryFields` — true for `create`/`update`, which take `--<field> <value>` for any
column. A top-level `options` map gives each flag's type, so a caller knows which take a value.

---

Canonical: https://www.zeyos.com/guide/cli/configuration

# Configuration

The CLI uses a layered configuration system for credentials, output preferences, and per-resource field display.

:::info Coverage Boundary
These settings apply to the CLI's curated resource registry. If you need a resource or request shape outside that registry, switch to [`@zeyos/client`](https://www.zeyos.com/guide/javascript-client/getting-started).
:::

## Credential Cascade

The CLI first decides **which credential set** is the base, then lets environment credential variables field-override on top. The base is chosen by the first match in this order:

| Priority | Source | Location |
|----------|--------|----------|
| 1 (highest) | `--profile <name>` flag | named profile (per command) |
| 2 | `ZEYOS_PROFILE` env var | named profile |
| 3 | Project pin | `.zeyos/profile` (walks up from CWD) |
| 4 | Local config file | `.zeyos/auth.json` (walks up from CWD) |
| 5 | Global active profile | `~/.config/zeyos/profiles.json` (`active`) |
| 6 (lowest) | Global config file | `~/.config/zeyos/credentials.json` |

On top of the chosen base, the credential **environment variables** (`ZEYOS_BASE_URL`, `ZEYOS_TOKEN`, …) always override individual fields — so you can keep `baseUrl` and client credentials in a profile while overriding just the token via `ZEYOS_TOKEN` in CI.

If you have never created a profile, nothing changes: the CLI falls through to the legacy local `.zeyos/auth.json` and global `credentials.json` exactly as before.

## Profiles

Profiles let you store several ZeyOS instances (e.g. `dev`, `prod`, `client-x`) and switch between them without re-running `login`. Each profile is a full credential set (URL, OAuth app, tokens) kept in `~/.config/zeyos/profiles.json`, with one marked **active**.

```bash
# Create profiles (connection params now; tokens via login)
zeyos profile add                         # interactive wizard
zeyos profile add dev  --base-url https://zeyos.example.com/dev
zeyos profile add prod --base-url https://cloud.zeyos.com/acme --client-id app --secret "$SECRET"

# Authenticate into a profile (also makes it active)
zeyos login --profile prod

# See and switch profiles
zeyos profile list                 # active marked with *, shows token status
zeyos profile use dev              # switch the global active profile
zeyos profile current             # what resolves right now, and why

# Per-project: pin a profile so cd-ing into a repo selects its instance
cd ~/work/acme && zeyos profile use prod --local   # writes ./.zeyos/profile

# One-off override on any command (beats env, pin, and active)
zeyos whoami --profile dev
zeyos list tickets --profile prod
```

`zeyos profile add <name> --from-current` snapshots whatever credentials are in effect (including tokens) into a new profile — handy for adopting an existing `.zeyos/auth.json` setup. Add `.zeyos/profile` to `.gitignore` alongside `.zeyos/auth.json`.

## Environment Variables

| Variable | Maps to | Description |
|----------|---------|-------------|
| `ZEYOS_BASE_URL` | `baseUrl` | ZeyOS platform URL |
| `ZEYOS_INSTANCE` | `instance` | ZeyOS instance name |
| `ZEYOS_CLIENT_ID` | `clientId` | OAuth application ID |
| `ZEYOS_CLIENT_SECRET` | `clientSecret` | OAuth application secret |
| `ZEYOS_TOKEN` | `accessToken` | Access token |
| `ZEYOS_REFRESH_TOKEN` | `refreshToken` | Refresh token |

**Example:**

```bash
# Use environment variables for CI/CD pipelines
export ZEYOS_BASE_URL="https://cloud.zeyos.com/demo"
export ZEYOS_TOKEN="your-access-token"

zeyos list tickets
```

If you want the CLI to auto-refresh expired access tokens, also provide `ZEYOS_CLIENT_ID`, `ZEYOS_CLIENT_SECRET`, and a refresh token via config or environment variables.

## Local Config File

The CLI stores credentials in `.zeyos/auth.json`, located in your project directory. When resolving config, the CLI walks up from the current working directory (like Git does for `.gitconfig`) until it finds a `.zeyos/auth.json` file.

**File format:**

```json
{
  "baseUrl": "https://cloud.zeyos.com/demo",
  "clientId": "myapp",
  "clientSecret": "...",
  "accessToken": "...",
  "refreshToken": "...",
  "expiresAt": 1735568880,
  "refreshTokenExpiresAt": 1738160880,
  "dateFormat": "YYYY-MM-DD"
}
```

This file is created automatically when you run `zeyos login`.

`expiresAt` and `refreshTokenExpiresAt` are stored as Unix timestamps in **seconds**.

:::warning
Always add `.zeyos/auth.json` to your `.gitignore` — it contains access tokens and secrets:
```gitignore
.zeyos/auth.json
```
:::

## Global Config File

For credentials shared across all projects, use the global config:

```bash
zeyos login --global
```

This saves to `~/.config/zeyos/credentials.json` (same format as the local file). Local config values override global values field-by-field, so a project can override only the fields it needs.

## Resource Field Configuration

Customize which fields are displayed for each resource type. The CLI supports per-resource configs that control the `list` and `get` output.

### Config Cascade

Resource configs are also resolved in priority order:

| Priority | Location | Purpose |
|----------|----------|---------|
| 1 (highest) | `.zeyos/api/<resource>.json` | Project-specific overrides |
| 2 | `~/.zeyos/api/<resource>.json` | User-wide preferences |
| 3 (lowest) | `cli/config/<resource>.json` | Shipped defaults |

### Config Format

```json
{
  "list": {
    "fields": {
      "ID": "ID",
      "Name": "lastname",
      "City": "contact.city",
      "Country": "contact.country",
      "Agent": "assigneduser.name",
      "Modified": "lastmodified"
    }
  },
  "get": {
    "fields": [
      "ID", "name", "status", "priority",
      "description", "duedate", "lastmodified"
    ],
    "params": {
      "extdata": 1,
      "tags": 1
    }
  }
}
```

**`list.fields`** — An object mapping display aliases to API field paths. Keys become column headers; values are the actual API fields (supports dot-notation for joins).

**`get.fields`** — An array of field names to display in the detail view. Use `--all` to override and show everything.

**`get.params`** — An object of query parameters to include by default. These are sent as URL query parameters (e.g. `?extdata=1&tags=1`) and control which additional data sections are returned. Common params: `extdata` (extended data fields), `tags` (record tags).

:::note
The `--extdata` and `--tags` CLI flags override the corresponding values in `get.params`. For example, running `zeyos get ticket 42` with `"params": {"extdata": 1}` in the config is equivalent to running `zeyos get ticket 42 --extdata`.
:::

### Shipped Defaults

The CLI ships with configs for commonly used resources:

| File | Resource |
|------|----------|
| `cli/config/ticket.json` | Tickets |
| `cli/config/account.json` | Accounts |
| `cli/config/task.json` | Tasks |
| `cli/config/project.json` | Projects |
| `cli/config/item.json` | Items |

Resources without a config file return all fields from the API.

:::tip Customizing for Your Project
Create `.zeyos/api/ticket.json` in your project to override the default field display:
```bash
mkdir -p .zeyos/api
```
```json title=".zeyos/api/ticket.json"
{
  "list": {
    "fields": {
      "ID": "ID",
      "Title": "name",
      "Client": "account.lastname",
      "Sprint": "extdata.sprint",
      "Due": "duedate"
    }
  }
}
```
:::

## Date Formatting

Unix timestamps in table and record output are automatically formatted as human-readable dates. Configure the format in your auth config file:

```json title=".zeyos/auth.json"
{
  "baseUrl": "https://cloud.zeyos.com/demo",
  "dateFormat": "YYYY-MM-DD HH:mm"
}
```

**Available tokens:**

| Token | Output | Example |
|-------|--------|---------|
| `YYYY` | 4-digit year | `2026` |
| `MM` | 2-digit month | `03` |
| `DD` | 2-digit day | `02` |
| `HH` | 2-digit hour (24h) | `14` |
| `mm` | 2-digit minute | `30` |
| `ss` | 2-digit second | `00` |

**Default format:** `YYYY-MM-DD`

Date formatting only affects table and record output. JSON and YAML output always preserves raw Unix timestamps for programmatic use.

**Auto-detected date fields:** `duedate`, `lastmodified`, `creationdate`, `created`, `date`, `startdate`, `enddate`, and any field name ending in `date` or `modified`.

## Output Formats

| Flag | Format | Best For |
|------|--------|----------|
| *(none)* | Formatted table | Human reading in the terminal |
| `--json` | Pretty-printed JSON | Scripting, piping to `jq`, APIs |
| `--yaml` | YAML | Config files, readability |

**Table output** is the default — it shows aligned columns with optional date formatting and pagination info.

**JSON output** preserves all raw data exactly as returned by the API, including numeric timestamps and null values.

**YAML output** is similar to JSON but more readable for nested structures.

```bash
# Compare the outputs
zeyos get ticket 42           # Table with formatted dates
zeyos get ticket 42 --json    # Raw JSON with Unix timestamps
zeyos get ticket 42 --yaml    # YAML with readable structure
```

## Custom-field validation

`ZEYOS_CUSTOM_FIELDS_FILE` is an **absolute path** to a UTF-8 JSON snapshot with exactly these top-level properties:

```json
{
  "platform": "https://cloud.zeyos.com/your-instance",
  "customFields": {
    "accounts": {
      "region": { "type": "string", "enum": ["EMEA", "APAC"] }
    },
    "tickets": {}
  }
}
```

This is an illustrative contract; replace it with reviewed definitions from your instance. Keep private snapshots outside tracked files and omit credentials and record values. Use canonical API resource names. Populate keys from active `customfields` definitions: `context` identifies the owner, `source: 0` selects EXTDATA, and `reference` is the key. Verify pagination, visibility and application-defined keys before treating the map as complete. Display `type` is not a JSON type; specify constraints only when verified. See [the custom-field contract](https://www.zeyos.com/guide/javascript-client/making-requests#custom-field-contracts).

```bash
export ZEYOS_CUSTOM_FIELDS_FILE=/absolute/private/path/custom-fields.json
zeyos describe accounts --json
zeyos list accounts --fields 'ID,extdata.region' --limit 5 --dry-run --json
```

The loader checks the snapshot's full instance URL against the resolved request target, including the instance path; trailing slashes are normalized. A mismatch blocks dispatch. Malformed metadata is an error, never silently ignored. Offline `describe` requires no login and displays `customFieldSource.platform` with `live: false`; it describes the selected snapshot, not a live tenant. Confirm that provenance against the intended profile before using it.

`zeyos doctor agent` checks all of this at once: whether the variable is set, absolute and readable, whether the snapshot is valid, and whether its instance matches the configured one. A broken or mismatched snapshot exits `3`, because it would block every request. Doctor also reports the file's age from its modification time. The format has no field for a review date, and extra top-level properties are rejected, so record review details elsewhere.

A missing resource map means metadata is unavailable; `{}` means known empty. Both reject unverified keys, with different diagnostics. CLI and MCP reload the snapshot per command/tool call, so refresh the file after configuration changes. There is no automatic discovery or freshness guarantee. Reading all extdata with `--extdata` does not require a key allowlist; referencing or writing individual keys does.

These stricter checks require client/CLI 1.0.0 or later. Deploy the matching client and instructions together; older releases may accept these names unchecked.

---

Canonical: https://www.zeyos.com/guide/cli/getting-started

# CLI Getting Started

The ZeyOS CLI gives you fast, scriptable access to the ZeyOS REST API from your terminal. It is the default entry point for coding agents and shell automation against the CLI's curated resource registry.

## Installation

:::info Requirements
Node.js 18.3+ is required. The CLI has zero external dependencies beyond the bundled `@zeyos/client` package.
:::

### Published package (recommended)

Install the CLI globally from npm:

```bash
npm install -g @zeyos/cli
zeyos --help
```

### Running from source

If you are contributing to the CLI or need to run an unreleased version, clone the repository and run directly from the source tree:

```bash
# Clone the repository
git clone <repo-url>
cd client

# Run directly
./cli/bin/zeyos.mjs --help
```

For convenience, create a symlink so you can run `zeyos` from anywhere:

```bash
ln -s $(pwd)/cli/bin/zeyos.mjs /usr/local/bin/zeyos
```

## First Login

Authenticate with your ZeyOS instance using the `login` command:

```bash
# Interactive — prompts for URL, app ID, and secret
zeyos login

# Or provide all values upfront
zeyos login \
  --base-url https://cloud.zeyos.com/demo \
  --client-id myapp \
  --secret "$ZEYOS_CLIENT_SECRET"
```

For interactive use, omit `--secret`; the CLI prompts without echoing the secret to the terminal. Passing secrets as command-line arguments is best reserved for controlled automation.

**What happens:**

1. The CLI opens your browser to the ZeyOS authorization page
2. You log in and authorize the application
3. The CLI exchanges the authorization code for access + refresh tokens
4. Credentials are saved to `.zeyos/auth.json` in your project

:::tip Switching Instances
Use `--clean` to discard all saved credentials and start fresh:
```bash
zeyos login --clean
```
This is useful when switching between ZeyOS instances.
:::

## Your First Commands

Once logged in, you can start working with your data:

```bash
# Check who you're logged in as
zeyos whoami

# List tickets (table output by default)
zeyos list tickets

# Get a specific ticket with all details
zeyos get ticket 42 --all

# Count records
zeyos count accounts

# Create a new ticket
zeyos create ticket --name "Fix login bug" --status 0 --priority 3

# Update a ticket's status
zeyos update ticket 42 --status 4

# Delete a ticket
zeyos delete ticket 42
```

Inspect the curated CLI-supported resource set at any time:

```bash
zeyos resources
```

If the resource you need does not appear there, switch to [`@zeyos/client`](https://www.zeyos.com/guide/javascript-client/getting-started).

## Discover the Schema

Inspect a resource's fields, types, foreign keys, and enum values before querying. This works offline -- no login required -- so it is a fast way to learn the data model:

```bash
zeyos describe tickets
zeyos describe accounts --json
```

## Install Agent Skills

If you are driving the CLI from a coding agent (Claude Code, Codex, opencode, Factory Droid, pi, …), install the bundled ZeyOS skill packs so the agent picks up the right conventions:

```bash
zeyos skills list                 # see the available skills
zeyos skills install zeyos        # interactive: pick an agent, then local vs. global
zeyos skills install zeyos --target claude --global   # or skip the prompts with flags
zeyos skills verify --target claude --global          # check the installed files
```

See the [Commands Reference](https://www.zeyos.com/guide/cli/commands#skills) for details.

## Connect an MCP Host

The package also includes the `zeyos-mcp` stdio server for Claude Desktop and other MCP hosts. It mirrors the CLI’s schema validation, aliases, filters, presets, and credential profiles; see the [MCP Server guide](https://www.zeyos.com/guide/cli/mcp-server) for configuration and the write-safety gate.

## Working with Filters

Query specific records using JSON filter expressions:

```bash
# Tickets with status = 1 (In Progress)
zeyos list tickets --filter '{"status":1}'

# Combine multiple criteria (AND logic)
zeyos list tickets --filter '{"status":1,"priority":3}'

# Count matching records
zeyos count tickets --filter '{"status":1}'
```

For larger filters, store the JSON in a file and use `--filter-file`:

```bash
zeyos list tickets --filter-file ./filters/open-tickets.json --json
zeyos count tickets --filter-file ./filters/open-tickets.json --json
```

For normal operational views, include `visibility: 0` on resources that have the column:

```bash
zeyos list tickets --filter '{"visibility":0,"status":1}'
```

:::warning
`transactions` and its billing/procurement entities, plus `payments`, `messages`, `actionsteps`
and `addresses`, have **no** `visibility` column — filtering on it there returns an opaque
HTTP 400. Run `zeyos describe <entity>` to see which columns a resource actually has.
:::

## Sorting and Pagination

```bash
# Sort by name ascending, then by last modified descending
zeyos list tickets --sort "+name,-lastmodified"

# Limit results and paginate
zeyos list tickets --limit 10
zeyos list tickets --limit 10 --offset 10   # page 2
```

When results fill the page limit, the CLI automatically shows pagination info:

```
Showing 1–10 of 47  (--offset 10 for next page)
```

## Output Formats

Choose the format that fits your workflow:

```bash
# Default: formatted table (human-readable)
zeyos list tickets

# JSON: for scripting and piping
zeyos list tickets --json

# YAML: for config-friendly output
zeyos list tickets --yaml
```

:::tip Piping
JSON output works great with tools like `jq`:
```bash
zeyos list tickets --json | jq '.[].name'
```
:::

## JSON-First Automation

For coding agents and non-interactive scripts, prefer `--json` output and JSON-first writes:

```bash
zeyos create ticket --data '{"name":"Fix login bug","status":0,"priority":3,"visibility":0}' --json
zeyos update ticket 42 --data '{"status":4}' --json
```

`zeyos whoami --json` does not print access tokens. If a local tool explicitly needs the current token, use `zeyos whoami --show-token --json` and treat the output as a secret.

## Credential Storage

Tokens are saved to `.zeyos/auth.json` in your project directory (or the nearest parent that has one). For global credentials shared across projects:

```bash
zeyos login --global
```

This saves to `~/.config/zeyos/credentials.json` instead.

:::warning
Add `.zeyos/auth.json` to your `.gitignore` — it contains access tokens.
:::

## Next Steps

- **[Coding Agents](https://www.zeyos.com/guide/agent-workflows/coding-agents)** -- CLI-first workflows and escalation guidance
- **[Commands Reference](https://www.zeyos.com/guide/cli/commands)** — Full reference for every command
- **[Configuration](https://www.zeyos.com/guide/cli/configuration)** — Config files, environment variables, and resource field customization
- **[MCP Server](https://www.zeyos.com/guide/cli/mcp-server)** — Connect Claude Desktop or another stdio MCP host

---

Canonical: https://www.zeyos.com/guide/cli/mcp-server

# MCP Server

`zeyos-mcp` is the zero-dependency Model Context Protocol server included with `@zeyos/cli`. It exposes the CLI’s resource registry, aliases, operator normalization, business presets, local schema validation, and credential/profile resolution to MCP hosts such as Claude Desktop.

The server uses newline-delimited JSON-RPC 2.0 over stdio. It writes protocol messages to stdout and reserves stderr for diagnostics.

It supports MCP `2025-06-18` and retains `2025-03-26` negotiation for older hosts.
The structured output contract follows the [MCP tools specification](https://modelcontextprotocol.io/specification/2025-06-18/server/tools).

## Install and run

Install the package globally to make both executables available:

```bash
npm install -g @zeyos/cli
zeyos-mcp
```

To run it without a global install, ask `npx` to install the package and invoke its second binary explicitly:

```bash
npx -y -p @zeyos/cli zeyos-mcp
```

Running `npx -y @zeyos/cli` by itself selects the package’s default `zeyos` executable, not `zeyos-mcp`.

## Host configuration

The server uses exactly the same environment variables, named profiles, project pin, and stored credential files as the CLI. A Claude Desktop configuration using environment credentials looks like this:

```json
{
  "mcpServers": {
    "zeyos": {
      "command": "npx",
      "args": ["-y", "-p", "@zeyos/cli", "zeyos-mcp"],
      "env": {
        "ZEYOS_BASE_URL": "https://cloud.zeyos.com/your-instance",
        "ZEYOS_TOKEN": "YOUR_ACCESS_TOKEN"
      }
    }
  }
}
```

For an existing named CLI profile, avoid copying credentials into the host configuration:

```json
{
  "mcpServers": {
    "zeyos": {
      "command": "zeyos-mcp",
      "env": {
        "ZEYOS_PROFILE": "production"
      }
    }
  }
}
```

The same `command`, `args`, and `env` shape works in generic stdio MCP hosts. The server starts and provides schema/discovery tools even when credentials are missing; authenticated calls return the CLI’s actionable configuration guidance as a tool error.

For individual custom-field references and writes, add `ZEYOS_CUSTOM_FIELDS_FILE` to the host environment with an absolute path to the [instance-specific snapshot](https://www.zeyos.com/guide/cli/configuration#custom-field-validation). `describe_resource` returns `customFields` and `customFieldSource` (snapshot URL, `live: false`). The server reloads metadata per call and checks the resolved instance before dispatch. Joined paths are checked through each foreign key to the terminal field; missing custom metadata returns an actionable error. Discovery remains offline and does not certify that the snapshot is current or complete.

## Tools

| Tool | Purpose |
|---|---|
| `list_resource_types` | List canonical resource names, descriptions, and presets. |
| `describe_resource` | Inspect local fields, types, enums, aliases, presets, and operations. |
| `find_records` | Resolve human-readable names to record IDs with full-text search. |
| `list_records` | Query and paginate records with normalized filters, presets, fields, sorting, or search. |
| `count_records` | Count records server-side without fetching every row. |
| `sum_records` | Page through matches and sum one numeric field. |
| `get_record` | Fetch one exact record by ID. |
| `create_record` | Create one validated record; hidden unless writes are enabled. |
| `update_record` | Update one validated record; hidden unless writes are enabled. |

There is deliberately no delete tool.

## Filters and structured results

`list_records`, `count_records`, and `sum_records` accept canonical `filters`:

```json
{
  "name": "list_records",
  "arguments": {
    "resource": "tickets",
    "filters": { "visibility": 0, "status": 4 },
    "limit": 25
  }
}
```

The singular `filter` spelling remains a deprecated alias and produces a warning
in a successful structured result. Supplying both keys fails validation before
authentication or an API request. Upstream requests contain only `filters`.
The human CLI flag remains `--filter`; request previews use `--dry-run`.

Every tool advertises an `outputSchema` and behavior annotations. The result
contains `structuredContent: { ok: true, data: ... }` on success, or
`structuredContent: { ok: false, error: { code, message, actions? } }` together
with `isError: true` for tool failures. Hosts can branch on `ok` and consume
`data` without parsing JSON from text. The existing text payload is retained for
compatibility. Resource discovery's `data` is an array; lists contain
`data.rows`; count and sum tools expose their named values inside `data`.

Billing `invoice` / `invoices` aliases resolve to `billing_invoice`, whose
transaction type is bound to `3`. Use `documents` for formal document artifacts
and `procurement_invoices` for supplier invoices. The aliases never route a
billing query to document records.

## Write gating

Read tools are always advertised. To expose `create_record` and `update_record`, set the gate when starting the host:

```json
{
  "env": {
    "ZEYOS_MCP_ALLOW_WRITES": "1"
  }
}
```

Write access remains off for every other value. Enabling the tools does not replace normal agent safeguards: resolve and read the exact target first, scope the payload, and obtain authorization for the specific write.

---

Canonical: https://www.zeyos.com/guide/javascript-client/authentication

# Authentication

The ZeyOS client supports multiple authentication strategies that can be selected via the `auth.mode` configuration option. The client determines how to authenticate each request based on the chosen mode and the security requirements of the target API operation.

## Auth Modes

| Mode | Behavior |
|------|----------|
| `auto` (default) | Tries bearer token first, attempts token refresh on 401, then falls back to session cookies |
| `oauth` | Bearer token only; uses access tokens from the configured token store |
| `session` | Session cookies only; relies on browser session or an explicit cookie value |
| `none` | No authentication; requests are sent without any credentials |

## Session Mode

Session mode is the simplest option when the user is already logged into ZeyOS in the same browser. The client sends requests with `credentials: 'include'`, allowing the browser to attach the existing session cookie automatically.

```js
import { createZeyosClient } from '@zeyos/client';

const client = createZeyosClient({
  platform: 'https://cloud.zeyos.com/demo/',
  auth: {
    mode: 'session',
    session: { enabled: true, credentials: 'include' },
  },
});

// Requests use the browser's session cookie
const tickets = await client.api.listTickets({ limit: 10 });
```

:::tip When to use session mode
Session mode is ideal for apps embedded inside the ZeyOS platform, browser extensions, or any scenario where the user has already authenticated with ZeyOS in the same browser. No tokens need to be stored or managed -- the browser handles everything.
:::

## Token Mode

For standalone applications, server-side scripts, or situations where you have pre-obtained tokens, use OAuth mode with a token store.

```js
import { createZeyosClient, MemoryTokenStore } from '@zeyos/client';

const tokenStore = new MemoryTokenStore({
  accessToken: 'your-access-token',
  refreshToken: 'your-refresh-token',
});

const client = createZeyosClient({
  platform: 'https://cloud.zeyos.com/demo/',
  auth: {
    mode: 'oauth',
    oauth: { tokenStore },
  },
});

// Requests use the bearer token from the store
const accounts = await client.api.listAccounts({ limit: 25 });
```

This is the safest default when you already have a valid access token. When `autoRefresh` is enabled and the token has expired or a bearer request receives a 401 response, the client uses the refresh token to obtain a new access token. This requires `clientId` and `clientSecret` to be configured:

```js
const client = createZeyosClient({
  platform: 'https://cloud.zeyos.com/demo/',
  auth: {
    mode: 'oauth',
    oauth: {
      tokenStore,
      autoRefresh: true,
      clientId: 'your-client-id',
      clientSecret: 'your-client-secret',
    },
  },
});
```

## Token Store

The built-in `MemoryTokenStore` keeps tokens in memory. Tokens are lost when the process exits or the page is refreshed.

For persistent storage, implement a custom token store with `get()` and `set()` methods:

```js
class LocalStorageTokenStore {
  async get() {
    const raw = localStorage.getItem('zeyos_tokens');
    return raw ? JSON.parse(raw) : null;
  }

  async set(tokenSet) {
    if (tokenSet) {
      localStorage.setItem('zeyos_tokens', JSON.stringify(tokenSet));
    } else {
      localStorage.removeItem('zeyos_tokens');
    }
  }
}

const client = createZeyosClient({
  platform: 'https://cloud.zeyos.com/demo/',
  auth: {
    mode: 'oauth',
    oauth: {
      tokenStore: new LocalStorageTokenStore(),
    },
  },
});
```

If you want `autoRefresh: true`, add `clientId` and `clientSecret` in a trusted environment. For browser-only apps, prefer session mode or a backend-assisted token flow rather than embedding a client secret in shipped code.

A token set object contains the following fields:

| Field | Type | Description |
|-------|------|-------------|
| `accessToken` | `string` | The OAuth 2.0 access token |
| `refreshToken` | `string \| null` | The refresh token (if issued) |
| `tokenType` | `string` | Token type, typically `'Bearer'` |
| `expiresIn` | `number \| null` | Token lifetime in seconds from issuance |
| `expiresAt` | `number \| null` | Absolute expiry as a Unix timestamp (seconds) |
| `obtainedAt` | `number` | Unix timestamp when the token was obtained |
| `refreshTokenExpiresIn` | `number \| null` | Refresh token lifetime in seconds |
| `refreshTokenExpiresAt` | `number \| null` | Refresh token absolute expiry (Unix timestamp) |

### Token Set Helpers

Two helpers are exported for working with token sets directly — handy when persisting and re-hydrating tokens:

- **`normalizeTokenSet(input)`** — normalize a partial token set into the shape above, tolerating both camelCase and snake_case fields (`access_token`, `expires_in`, …). Returns `null` if the input has neither an access nor a refresh token.
- **`tokenResponseToTokenSet(response)`** — convert a raw OAuth 2.0 token-endpoint response into a normalized token set (an alias of `normalizeTokenSet`).

```js
import { normalizeTokenSet } from '@zeyos/client';

const stored = JSON.parse(localStorage.getItem('zeyos_tokens') ?? 'null');
const tokenSet = normalizeTokenSet(stored);   // tolerant of snake_case, returns null if empty
```

## OAuth 2.0 Flow

For server-side or other trusted applications that can safely hold OAuth client credentials, use the standard OAuth 2.0 authorization code flow:

```js
import { createZeyosClient, MemoryTokenStore } from '@zeyos/client';

const tokenStore = new MemoryTokenStore();

const client = createZeyosClient({
  platform: 'https://cloud.zeyos.com/demo/',
  auth: {
    mode: 'oauth',
    oauth: {
      tokenStore,
      clientId: 'your-client-id',
      clientSecret: 'your-client-secret',
    },
  },
});

// 1. Build the authorization URL and redirect the user
const url = client.oauth2.buildAuthorizationUrl({
  redirectUri: 'https://myapp.com/callback',
  state: crypto.randomUUID(),
});

// 2. After the user authorizes, exchange the code for tokens
const tokenSet = await client.oauth2.exchangeAuthorizationCode({
  code: 'authorization-code-from-callback',
  redirectUri: 'https://myapp.com/callback',
});

// 3. Tokens are automatically stored in the tokenStore
// All subsequent API calls use the new tokens
const user = await client.api.listAccounts({ limit: 1 });
```

You can also parse the callback URL to extract the authorization code and detect errors:

```js
const callback = client.oauth2.parseAuthorizationCallback(
  'https://myapp.com/callback?code=abc123&state=xyz'
);

if (callback.isError) {
  console.error(callback.error, callback.errorDescription);
} else {
  const tokenSet = await client.oauth2.exchangeAuthorizationCode({
    code: callback.code,
  });
}
```

:::warning
The current OAuth helper expects `clientSecret` for authorization-code exchange and token refresh. Do not run that flow directly in shipped browser-only code. Use session mode in the browser or move the code exchange and refresh logic to a backend.
:::

## Token Operations

The `client.oauth2` namespace provides methods for managing tokens throughout their lifecycle:

```js
// Refresh the current access token
const newTokenSet = await client.oauth2.refreshToken();

// Revoke a token (access or refresh)
await client.oauth2.revokeToken({ token: 'token-to-revoke' });

// Introspect a token to check its validity and metadata
const info = await client.oauth2.introspectToken({ token: 'token-to-check' });
```

You can also manage the token set directly through the `client.auth` interface:

```js
// Read the current token set
const tokenSet = await client.auth.getTokenSet();

// Replace the stored token set
await client.auth.setTokenSet({
  accessToken: 'new-access-token',
  refreshToken: 'new-refresh-token',
});

// Clear all stored tokens
await client.auth.clearTokenSet();
```

## Auto Mode

The default `auto` mode implements an intelligent fallback chain:

1. **Bearer token** -- if the token store contains an access token, use it
2. **Token refresh** -- if the stored token is expired or the bearer request returns 401 and a refresh token is available (with `clientId`/`clientSecret`), refresh the access token
3. **Session fallback** -- if bearer authentication is unavailable or fails, fall back to session cookies (when `session.enabled` is `true`)

This makes `auto` mode the most flexible option. It works seamlessly when tokens are available and gracefully degrades to session authentication when they are not.

```js
const client = createZeyosClient({
  platform: 'https://cloud.zeyos.com/demo/',
  auth: {
    mode: 'auto', // this is the default
    oauth: {
      tokenStore,
      clientId: 'your-client-id',
      clientSecret: 'your-client-secret',
      autoRefresh: true,
    },
    session: { enabled: true, credentials: 'include' },
  },
});
```

:::warning
Never embed `clientSecret` in browser-facing JavaScript for production applications. Client secrets should only be used in server-side code or trusted environments. For browser-only apps, use session mode or a backend proxy that handles token exchange.
:::

## Legacy Authentication (`client.legacyAuth`)

The `client.legacyAuth` namespace exposes the ZeyOS legacy session authentication API. These operations target a separate endpoint (`/{INSTANCE}/auth/v1/`) and are primarily used by ZeyOS platform apps that manage session lifecycle directly rather than through OAuth 2.0.

| Method | Description |
|--------|-------------|
| `client.legacyAuth.login(input)` | Start a session using username and password credentials |
| `client.legacyAuth.logout()` | Terminate the current session |
| `client.legacyAuth.verify()` | Check whether the current session is still valid (HEAD request) |
| `client.legacyAuth.getUserInfo()` | Retrieve information about the currently authenticated user |

```js
// Verify current session validity
await client.legacyAuth.verify();

// Get info about the logged-in user
const userInfo = await client.legacyAuth.getUserInfo();

// Log out
await client.legacyAuth.logout();
```

:::info
For most integrations, prefer OAuth 2.0 (via `client.oauth2`) or session cookie mode (via `auth.mode: 'session'`). The `legacyAuth` API is primarily intended for internal ZeyOS platform use and scripts that run within an already-authenticated ZeyOS session.
:::

---

Canonical: https://www.zeyos.com/guide/javascript-client/getting-started

# Getting Started

The ZeyOS JavaScript client provides a generated, dependency-light interface to the ZeyOS REST API. It works in both browser environments and Node.js 18+, with built-in support for OAuth 2.0, session-based authentication, and automatic token refresh.

Use the JavaScript client when:

- the CLI resource registry is not enough
- you are building a browser UI or a server-side integration
- you need request-level control through `client.request()`
- you want access to the full generated API surface instead of the CLI's curated subset

## Installation

:::info ESM only
`@zeyos/client` is an ES module (ESM) package. Use `import` (or dynamic `import()`) to load it. CommonJS `require()` is not supported.
:::

The client is distributed as an ES module source package. If you are working inside this repository, import it directly from the source tree:

```js
import { createZeyosClient } from './src/index.js';
```

If you are consuming it from another project, install the package and import it by name:

```bash
npm install @zeyos/client
```

```js
import { createZeyosClient } from '@zeyos/client';
```

The client has zero runtime dependencies and relies only on the standard `fetch` API available in modern browsers and Node.js 18+.

## Creating a Client

The `createZeyosClient` factory function returns a frozen client object with generated API methods, OAuth 2.0 helpers, and low-level request capabilities.

```js
import { createZeyosClient } from '@zeyos/client';

const client = createZeyosClient({
  platform: 'https://cloud.zeyos.com/demo/',
  validate: true,
  auth: {
    mode: 'session',
    session: { enabled: true, credentials: 'include' },
  },
});

const tickets = await client.api.listTickets({ limit: 10 });
```

Keep `validate: true` in new code. It checks generated methods and known-operation `client.request()` calls against the bundled schema before anything is sent, and throws `ZeyosValidationError` with a correction hint. It is off by default for compatibility.

### Custom Fields Belong to One Instance

Custom `extdata` keys differ between ZeyOS instances, so the bundled schema cannot know them. With validation on, naming an individual key requires a reviewed `customFields` contract for that instance:

```js
const client = createZeyosClient({
  platform: 'https://cloud.zeyos.com/demo/',
  validate: true,
  customFields: {
    accounts: { region: { type: 'string', enum: ['EMEA', 'APAC'] } }, // Reviewed keys for this instance
    tickets: {},                                                      // Known to have none
  },
});

client.schema.describe('accounts').customFields; // The definitions supplied above
```

The client does not fetch these definitions or check that they belong to the instance you are connected to. That is your responsibility: see [custom-field contracts](https://www.zeyos.com/guide/javascript-client/making-requests#custom-field-contracts) for binding a contract to its instance. Reading the whole collection with `extdata: 1` needs no contract.

The returned `client` object exposes:

- **`client.api`** -- Generated methods for all standard REST operations (e.g. `listTickets`, `createAccount`, `getTask`)
- **`client.oauth2`** -- OAuth 2.0 token operations and authorization URL helpers
- **`client.legacyAuth`** -- Legacy session authentication operations (`login`, `logout`, `verify`, `getUserInfo`)
- **`client.request()`** -- Low-level escape hatch for custom or advanced requests. A method and path call skips schema validation; see [Low-Level Requests](https://www.zeyos.com/guide/javascript-client/making-requests#low-level-requests)
- **`client.schema`** -- Runtime introspection and input validation (`describe`, `fields`, `resources`, `operations`, `validate`); see [Making Requests](https://www.zeyos.com/guide/javascript-client/making-requests#schema-introspection-and-validation)
- **`client.auth`** -- Token management (`getTokenSet`, `setTokenSet`, `clearTokenSet`)
- **`client.metadata`** -- Read-only info about the generated client (`generatedAt` timestamp, `services` array)

Alongside the `createZeyosClient` factory, the package exports a few named helpers and error types you can import directly:

```js
import {
  createZeyosClient,
  MemoryTokenStore,        // in-memory token store (or implement get()/set() yourself)
  ZeyosApiError,           // thrown on non-2xx responses
  ZeyosValidationError,    // thrown by pre-flight validation (validate: true)
  normalizeListResult,     // normalise a list response to { data, count? }
  normalizeCountResult,    // normalise a count response to a number
  normalizeTokenSet,       // normalise a raw token object to a TokenSet
  tokenResponseToTokenSet, // map an OAuth token response to a TokenSet
} from '@zeyos/client';
```

## Platform Configuration

The `platform` option tells the client where your ZeyOS instance lives. There are three ways to specify it:

### URL String

Pass the full URL to your ZeyOS instance. The client extracts the origin and instance name automatically.

```js
const client = createZeyosClient({
  platform: 'https://cloud.zeyos.com/demo/',
});
```

### Preset

Use a named preset for the ZeyOS cloud platform, combined with an `instance` name:

```js
const client = createZeyosClient({
  platform: 'live',
  instance: 'demo',
});
```

The `live` preset resolves to `https://cloud.zeyos.com`.

### Object

For full control, pass an object with `origin` and `instance`:

```js
const client = createZeyosClient({
  platform: {
    origin: 'https://cloud.zeyos.com',
    instance: 'demo',
  },
});
```

## Features

- **Zero dependencies** -- no external packages; only the standard `fetch` API is required
- **Browser + Node.js 18+** -- works in any environment with global `fetch`
- **Auto token refresh** -- transparently refreshes expired access tokens before requests or after bearer 401 responses
- **Generated API methods** -- the broader generated API surface is available as `client.api.<operationId>()`
- **Session + OAuth support** -- choose between cookie-based sessions, bearer tokens, or automatic fallback
- **Low-level escape hatch** -- `client.request()` for custom endpoints and advanced use cases

## Configuration Reference

| Option | Type | Description |
|--------|------|-------------|
| `platform` | `string \| { origin?: string, instance?: string, preset?: string, url?: string }` | ZeyOS instance URL, preset name (e.g. `'live'`), or object with `origin`/`instance` (and optionally `preset` or `url`) |
| `platform.origin` | `string` | Base origin URL (e.g. `'https://cloud.zeyos.com'`) |
| `platform.instance` | `string` | Instance name (e.g. `'demo'`) |
| `platform.preset` | `string` | Named preset (e.g. `'live'`) used when `origin` is omitted |
| `platform.url` | `string` | Full instance URL as an alternative to `origin`+`instance` |
| `instance` | `string` | Top-level instance name shortcut (used with preset-style `platform`) |
| `auth.mode` | `string` | Authentication mode: `'auto'` (default), `'oauth'`, `'session'`, or `'none'` |
| `auth.oauth.clientId` | `string` | OAuth 2.0 client ID for token operations |
| `auth.oauth.clientSecret` | `string` | OAuth 2.0 client secret for token operations |
| `auth.oauth.tokenStore` | `TokenStore` | Token storage backend (must implement `get()` and `set()`) |
| `auth.oauth.autoRefresh` | `boolean` | Automatically refresh access tokens before requests or after bearer 401 responses (default: `true`) |
| `auth.session.enabled` | `boolean` | Enable session-based authentication (default: `true`) |
| `auth.session.credentials` | `string` | Fetch credentials mode: `'include'`, `'same-origin'`, or `'omit'` |
| `auth.session.cookie` | `string \| () => string \| Promise<string>` | Explicit session cookie value or async function returning one (Node.js) |
| `headers` | `HeadersInit` | Default headers applied to every request |
| `fetch` | `typeof fetch` | Custom `fetch` implementation (defaults to `globalThis.fetch`) |
| `retry` | `{ maxRetries?: number, retryOn?: number[], baseDelayMs?: number, maxDelayMs?: number } \| false` | Retry policy. By default, up to two retries: `429` for any operation, `503` for reads only. Pass `false` to disable. See [Making Requests](https://www.zeyos.com/guide/javascript-client/making-requests#retries-and-rate-limiting) |
| `retryOnNetworkError` | `boolean` | Retry network errors, timeouts and `retryOn` statuses other than `429` for every operation (`true`) or none (`false`). Omit it to allow this for reads only |
| `timeoutMs` | `number` | Deadline for each attempt, in milliseconds. Default: none |
| `validate` | `boolean` | When `true`, validate each request against the schema before sending and throw `ZeyosValidationError` on problems (default: `false`). See [Making Requests](https://www.zeyos.com/guide/javascript-client/making-requests#schema-introspection-and-validation) |
| `customFields` | `Record<string, Record<string, { type?: string, enum?: Array<string \| number \| boolean>, description?: string }>>` | Reviewed custom-field keys per canonical resource, for one instance. See [custom-field contracts](https://www.zeyos.com/guide/javascript-client/making-requests#custom-field-contracts) |

## Next Steps

- **[Application Developers](https://www.zeyos.com/guide/tutorials/application-developers)** -- choose the browser or server-side integration path
- **[Authentication](https://www.zeyos.com/guide/javascript-client/authentication)** -- learn about session mode, OAuth 2.0 flows, and token management
- **[Making Requests](https://www.zeyos.com/guide/javascript-client/making-requests)** -- explore CRUD operations, filtering, sorting, pagination, and error handling
- **[Practical Guide](https://www.zeyos.com/guide/javascript-client/practical-guide)** -- real-world patterns and gotchas discovered during implementation
- **[Sample Application](https://www.zeyos.com/guide/sample-apps/kanban)** -- run the included Kanban board demo to see the client in action

---

Canonical: https://www.zeyos.com/guide/javascript-client/making-requests

# Making API Requests

The ZeyOS client generates methods for every REST operation defined in the ZeyOS OpenAPI specification. These methods provide a clean, high-level interface for all CRUD operations, filtering, sorting, and pagination.

## Generated Methods

All standard API operations are available as `client.api.<operationId>(input, options?)`. The `operationId` corresponds directly to the operation name in the ZeyOS OpenAPI specification.

```js
// Examples of generated methods
client.api.listTickets(...)
client.api.getTicket(...)
client.api.createTicket(...)
client.api.updateTicket(...)
client.api.deleteTicket(...)
client.api.listAccounts(...)
client.api.createTask(...)
```

Each method accepts an `input` object where you can mix path parameters, query parameters, and request body fields in a single flat object. The client automatically routes each property to the correct location based on the operation's parameter definitions.

The examples below assume a client created with `validate: true`, so mistakes are caught before a request is sent:

```js
const client = createZeyosClient({ platform: 'live', instance: 'demo', validate: true });
```

## CRUD Operations

### List Records

Retrieve collections of records with optional filters, sorting, and pagination:

```js
const tickets = await client.api.listTickets({
  fields: ['ID', 'name', 'status', 'priority', 'duedate'],
  filters: { status: 1, visibility: 0 },
  sort: ['-lastmodified'],
  limit: 50,
});
```

### Get a Single Record

Fetch a specific record by ID. Additional flags control which related data is included:

```js
const ticket = await client.api.getTicket({
  ID: 42,
  extdata: 1,
  tags: 1,
});
```

### Create a Record

Create a new record by passing the required fields. For operations **without** path parameters (like create), you can pass all fields as a flat object:

```js
const newTicket = await client.api.createTicket({
  name: 'Fix login bug',
  status: 0,
  priority: 3,
  description: 'Users cannot log in with SSO',
  visibility: 0,
});
```

:::caution Required fields the spec does not declare
Some columns are `NOT NULL` with no database default, so a create that omits them fails server-side with an opaque HTTP 500 — even though the OpenAPI spec marks nothing as required. Most notably, **creating an `account` requires a `currency`** (e.g. `"EUR"`). `client.schema.validate('createAccount', …)` now flags this before you send it.

```js
await client.api.createAccount({ lastname: 'Acme Corp', type: 1, currency: 'EUR' });
```
:::

### Update a Record

Update an existing record with a PATCH request. Pass the `ID` and changed fields in one object:

```js
await client.api.updateTicket({
  ID: 42,
  status: 4,
  priority: 4,
});
```

:::tip Explicit body is also supported
Use `body` or `data` when you want to separate URL parameters from payload fields manually:

```js
await client.api.updateTicket({ ID: 42, body: { status: 4, priority: 4 } });
```
:::

The PATCH response body contains the full updated record. Use it to confirm the server applied your changes:

```js
const updated = await client.api.updateTicket({
  ID: 42,
  status: 4,
});
console.log(updated.status); // 4 -- confirmed by the server
```

### Delete a Record

Delete a record by ID:

```js
await client.api.deleteTicket({ ID: 42 });
```

### Check Existence

Use a HEAD request to check whether a record exists without downloading the full response body:

```js
const exists = await client.api.existsTicket({ ID: 42 });
// Returns true if the record exists (2xx/3xx), throws ZeyosApiError on 404
```

## Field Selection

Control which fields are returned in list responses. This reduces payload size and improves performance.

### Array Form

Pass an array of field names to return only those fields:

```js
const result = await client.api.listAccounts({
  fields: ['ID', 'lastname', 'contact.city'],
  filters: { visibility: 0 },
});
```

### Object Form (with Aliases)

Pass an object to rename fields in the response. Keys become the output names, values are the source field paths:

```js
const result = await client.api.listAccounts({
  fields: {
    'Id': 'ID',
    'Name': 'lastname',
    'City': 'contact.city',
    'Agent': 'assigneduser.name',
  },
  filters: { visibility: 0 },
});
```

:::note
Dot-notation field paths (e.g. `contact.city`, `assigneduser.name`) allow you to select fields from related or nested objects.
:::

## Filtering

Pass filters as `filters` (plural). It covers every field type — scalars, enums and
GIN-indexed foreign keys:

```js
// Standard filtering -- works for all field types
const active = await client.api.listTickets({
  filters: { status: 4, visibility: 0 },
});

// Filter by foreign key field (e.g. project, account)
const projectTickets = await client.api.listTickets({
  filters: { visibility: 0, project: projectId },
});
```

### Why not `filter` (singular)?

The bundled OpenAPI spec names the parameter `filter`, but ZeyOS's
[published API documentation](https://www.zeyos.com/api/llms-full.txt) uses `filters` in every
worked example and never mentions the singular. There is one parameter; the spec is the
outlier.

Schema validation reflects that: `client.schema.validate()` and the CLI both reject `filter`
and point you at `filters`.

:::note
Validation is **off by default** in the JS client — pass `validate: true` to
`createZeyosClient()` (the CLI enables it for you). Without it, a `filter` key is forwarded
to the server as-is.
:::

### Full-Text Search

Use the `query` parameter to search across a resource's indexed text fields:

```js
const results = await client.api.listAccounts({
  fields: ['ID', 'lastname', 'contact.email'],
  filters: { visibility: 0 },
  query: 'acme',
  limit: 20,
});
```

## Distinct Results

Pass `distinct: true` to deduplicate result rows. This is useful when using dot-notation joins that may produce multiple rows per record:

```js
const result = await client.api.listAccounts({
  distinct: true,
  fields: ['ID', 'lastname', 'contact.country'],
  filters: { visibility: 0 },
});
```

## Sorting

Pass an array of field names prefixed with `+` (ascending) or `-` (descending):

```js
// Sort by last modified, newest first
const tickets = await client.api.listTickets({
  sort: ['-lastmodified'],
});

// Multi-field sort: priority descending, then name ascending
const tickets = await client.api.listTickets({
  sort: ['-priority', '+name'],
});
```

## Pagination

Use `limit` and `offset` to page through large result sets:

```js
// Get total count first
const countResult = await client.api.listTickets({
  count: true,
  filters: { status: 1, visibility: 0 },
});
// countResult contains the total number of matching records

// Fetch the first page
const page1 = await client.api.listTickets({
  limit: 50,
  offset: 0,
  filters: { status: 1, visibility: 0 },
  sort: ['+ID'],
});

// Fetch the second page
const page2 = await client.api.listTickets({
  limit: 50,
  offset: 50,
  filters: { status: 1, visibility: 0 },
  sort: ['+ID'],
});
```

Always set an explicit `sort` when paging. Without one, the API does not promise that rows keep their order between requests.

:::caution Offsets shift when data changes
`offset` counts rows at the moment of each request, and the API has no server-side cursor. If a record is added, removed or moved between two pages, a later page can skip or repeat rows. For jobs that must see every change, use [keyset pagination with a checkpoint](https://www.zeyos.com/guide/tutorials/server-side-integrations#3-fetch-one-page).
:::

:::tip
Use `count: true` to get the total number of matching records without fetching the full dataset. This is useful for building pagination controls.
:::

### Auto-pagination

To iterate an entire result set without managing `offset` yourself, use `client.paginate(operationId, input, opts)` — an async iterator that pages until a short/empty page (or `opts.max`) is reached. It removes the off-by-one and "I only got the first 1000 / 50 rows" mistakes the list caps otherwise invite.

```js
// Stream every matching ticket, one page at a time
for await (const ticket of client.paginate('listTickets', { filters: { visibility: 0 } }, { pageSize: 1000 })) {
  process(ticket);
}

// Eagerly collect up to a cap
const recent = await client.collect(
  'listTickets',
  { filters: { visibility: 0 }, sort: ['-lastmodified'] },
  { pageSize: 500, max: 2000 }
);
```

`opts`: `pageSize` (default 1000, clamped to the server max of 10000), `max` (stop after N records), and `requestOptions` (forwarded to each underlying call, e.g. `{ signal, timeoutMs }`). `collect` is the eager array form of `paginate`.

`paginate` pages by `offset`, so the caution above applies to it: use it for data that is stable during the read. It keeps no position, so a failed iteration restarts from the beginning.

## Normalising List Responses

List endpoints are not uniform across the full surface area. Depending on the endpoint and response mode, you may see:

- a plain array
- an object wrapper with `data`
- count metadata alongside the payload when `count: true` is used

The normalization helpers are useful when you want call sites to share one response-shape contract:

```js
import { normalizeCountResult, normalizeListResult } from '@zeyos/client';

// Without count -- result is a plain array
const raw = await client.api.listTickets({ filters: { visibility: 0 } });
const { data } = normalizeListResult(raw);
// data is always an array

// With count metadata -- result may include both data and count
const raw2 = await client.api.listTickets({ filters: { visibility: 0 }, count: true });
const { data: tickets, count } = normalizeListResult(raw2);
// tickets: array, count: number

// Count-only request -- result may be a number or an object with count metadata
const countOnly = await client.api.listTickets({ filters: { visibility: 0 }, count: true });
const total = normalizeCountResult(countOnly);
```

## Extended Data

Many ZeyOS entities support **extended data** (extdata) -- custom fields defined through the platform's extensibility features. By default, extended data fields are not included in API responses. To include them, pass `extdata: 1` as a parameter.

### Including Extended Data in List Requests

For list operations, `extdata` is sent as a body parameter:

```js
const tickets = await client.api.listTickets({
  fields: ['ID', 'name', 'status', 'priority', 'duedate'],
  filters: { status: 1, visibility: 0 },
  sort: ['-lastmodified'],
  limit: 50,
  extdata: 1,
});
```

### Including Extended Data in GET Requests

For single-record GET operations, `extdata` is sent as a query parameter:

```js
const ticket = await client.api.getTicket({
  ID: 42,
  extdata: 1,
  tags: 1,
});
```

### Selecting Specific Extended Data Fields

You can reference individual extended data fields using dot notation in the `fields` parameter. Use the `extdata.fieldname` syntax to select only the custom fields you need:

```js
const tickets = await client.api.listTickets({
  fields: {
    'Id': 'ID',
    'Name': 'name',
    'Region': 'extdata.region',
    'CustomerType': 'extdata.customer_type',
  },
  filters: { status: 1, visibility: 0 },
  limit: 50,
});
```

:::note
When you select specific `extdata.*` fields via the `fields` parameter, you do not need to pass `extdata: 1` separately -- the selected fields will be included automatically.
:::

## Expanding JSON and Binary Columns

The `expand` parameter is used to inline the contents of **JSON columns** or **binary/file columns** that are normally returned as references or omitted for performance reasons. This applies to structured data columns such as `binfile` on messages, `items` on transactions, or `data` on objects.

```js
// Expand the binary file content of a message
const message = await client.api.getMessage({
  ID: 123,
  expand: ['binfile'],
});

// Expand the items array on a transaction
const transaction = await client.api.getTransaction({
  ID: 456,
  expand: ['items'],
});
```

:::caution
Do not confuse `expand` with `extdata`. The `expand` parameter is strictly for JSON and binary columns -- it does not apply to extended data fields. To include extended data, use `extdata: 1` instead.
:::

## Schema Introspection and Validation

The `client.schema` surface lets you (or an AI agent) discover the data model and validate inputs **without any network call** -- it is built from the generated schema. This is the fastest way to learn fields, types, foreign keys, and enum values before issuing a request.

```js
client.schema.resources();                 // all resource names
client.schema.describe('tickets');          // { name, type, fields: { status: { type, enum }, account: { fk }, … } }
client.schema.fields('accounts');           // ['ID', 'lastname', 'firstname', 'type', …]
client.schema.operations('tickets');        // ['listTickets', 'getTicket', 'createTicket', …]
client.schema.resourceForOperation('listDunningNotices'); // 'dunning'
client.schema.suggestOperation('listDunning');            // 'listDunningNotices'
```

### Validating a Call

`client.schema.validate(operationId, input)` returns structured, self-correcting hints. It returns `{ valid, errors }` without a network call. Each dot-notation hop must be a documented foreign key and the terminal field must exist. This covers selected fields (including alias maps), nested filters, and sorting. Custom keys under `extdata` require an explicit field contract:

```js
const result = client.schema.validate('createAccount', { name: 'Acme', currency: 'EUR' });
// {
//   valid: false,
//   errors: [{ field: 'name', message: 'Unknown field "name". Did you mean "lastname"?', suggestion: 'lastname' }]
// }
```

It flags unknown fields (with a suggestion), `filter` used where `filters` is preferred, invalid enum values (listing the valid set), and missing required create fields. The ZeyOS spec carries no required-field metadata, so a curated supplement covers known NOT-NULL-without-default columns — notably `accounts` require `currency`:

```js
client.schema.validate('createAccount', { lastname: 'Acme' });
// { valid: false, errors: [{ field: 'currency', message: 'Missing required field "currency" for accounts …', suggestion: 'currency' }] }
```

### Pre-flight Validation

Set `validate: true` on the client to validate generated API methods and known-operation `client.request()` calls before they are sent and throw a `ZeyosValidationError` (rather than letting the server reject it):

```js
import { ZeyosValidationError } from '@zeyos/client';

const client = createZeyosClient({ platform: 'live', instance: 'demo', validate: true });

try {
  await client.api.createAccount({ name: 'Acme' });   // wrong field
} catch (err) {
  if (err instanceof ZeyosValidationError) {
    console.log(err.operationId);   // 'createAccount'
    console.log(err.errors);        // structured hints (see above)
  }
}
```

Validation is **off by default** in JavaScript and on by default in CLI/MCP data commands. Enable it in all new code. You can also enable it for one call by passing `{ validate: true }` as a request option.

A raw `client.request({ method, path })` without an operation ID has no schema mapping, so even `validate: true` does not check it. You lose the checks for unknown fields, `filter` versus `filters`, enum values, join paths, custom-field keys and required create fields. The first sign of a mistake is then a server error or a silently wrong result. Prefer `client.request({ service, operationId, body })`, which validates like a generated method.

### Custom-field contracts

Supply the complete reviewed key allowlist for each relevant API resource, using canonical resource names (for example `transactions`, not a CLI invoice alias):

```js
const client = createZeyosClient({
  auth: { mode: 'none' }, // Local validation only; configure auth for API access.
  validate: true,
  customFields: {
    accounts: { region: { type: 'string', enum: ['EMEA', 'APAC'] } },
    contacts: { preferred: { type: 'boolean' } },
    tickets: {},
  },
});

client.schema.validate('listAccounts', {
  fields: ['ID', 'contact.city', 'contact.extdata.preferred'],
  filters: { 'extdata.region': 'EMEA' },
}); // valid: true for this supplied contract

client.schema.validate('listAccounts', { fields: ['contact.invented'] });
// valid: false; unknown terminal field on contacts
client.schema.validate('listAccounts', { fields: ['extdata.invented'] });
// valid: false; unknown custom field
client.schema.validate('updateAccount', { id: 42, extdata: { region: 'INVALID' } });
// valid: false; value outside the configured enum (no request sent)
```

A definition `{}` validates only the key. Optional `type` supports JSON `string`, `number`, `integer`, `boolean`, `object`, and `array`; optional `enum` is an array of finite JSON scalar values, and `description` is a string. Constraints are explicit application contracts, **not** automatic conversions from the platform's display types. Equality and membership filter operands and write values use these constraints; ordering operands check type without requiring enum membership. This is not a complete filter-operator parser.

A missing resource map returns `custom_fields_unavailable`; an empty map means the resource is known to have no allowed custom keys. Unknown keys return `unknown_custom_field`. A key containing dots is matched literally after `extdata.`; it is not interpreted as a nested JSON property. `extdata: 1` on reads still requests the collection without naming individual keys. Writes use an `extdata` object; a known key set to `null` deletes that value on update. Joined query paths are not writable fields: update the related resource explicitly.

`client.schema.describe(resource).customFields` returns the supplied definitions, or `null` when unavailable. Metadata is copied at client creation; recreate the client after refreshing it. The library does not fetch tenant definitions or verify that your supplied contract belongs to the authenticated instance. CLI/MCP provide an [instance-bound snapshot loader](https://www.zeyos.com/guide/cli/configuration#custom-field-validation).

### Binding a Contract to Its Instance

A contract reviewed for one instance can wrongly pass or reject keys on another, so keep its origin next to it. The CLI and MCP do this for you: they refuse a snapshot from a different instance, and `describe` reports `customFieldSource` with the snapshot's `platform` and `live: false`. In JavaScript, `client.schema.describe()` has no `customFieldSource`, and the snapshot loader is not exported, so your application makes the same check. The [snapshot format](https://www.zeyos.com/guide/cli/configuration#custom-field-validation) lets one file serve both:

```js
import { readFile } from 'node:fs/promises';
import { createZeyosClient } from '@zeyos/client';

const normalize = (url) => url.replace(/\/+$/, '');

export async function createBoundClient({ platform, snapshotPath, auth }) {
  const snapshot = JSON.parse(await readFile(snapshotPath, 'utf8'));
  if (normalize(snapshot.platform) !== normalize(platform)) {
    throw new Error('Custom-field snapshot belongs to a different ZeyOS instance.');
  }

  const client = createZeyosClient({
    platform,
    auth,
    validate: true,
    customFields: snapshot.customFields,
  });

  // Same shape as the CLI: an offline snapshot, not a live reading of the tenant.
  return { client, customFieldSource: { platform: normalize(snapshot.platform), live: false } };
}
```

Log `customFieldSource` at startup so a run shows which contract it used. It proves where the snapshot was meant to apply, not that it is current. Keep your review date and source separately from the snapshot, because the CLI and MCP loader rejects any extra top-level property in that file. Refresh it after configuration changes.

To discover configured extdata definitions, query `customfields` using `context` for the owning entity, `source: 0` for EXTDATA, and `activity: 0` for active definitions. `reference` is the custom key. The `entity` column is the target of an entity-valued display field, **not** the owner. Page through every result and verify access/completeness; do not turn an incomplete or denied response into an empty allowlist. Retrieve `type` and `options` when reviewing value constraints, but do not infer JSON types or enums from labels alone. Record the instance, review date and provenance separately from credentials.

### Coverage and migration

The validator now rejects join suffixes and custom keys that older clients accepted unchecked. Existing validated integrations using `extdata.*` must supply the reviewed contract before upgrading. CLI/MCP users set `ZEYOS_CUSTOM_FIELDS_FILE`; do not suppress errors with `--no-validate` merely to make a request run.

Validation checks known metadata, not server permissions, record existence, all base-field types, every required parameter, join cardinality, arbitrary SQL, or native iXML/Zymba syntax. The curated create requirement remains account `currency`; the OpenAPI source does not supply a complete required-field contract. `group` and `having` return unsupported-validation errors rather than a silent pass. Operations with no bundled resource schema cannot receive field-level checks. A passing preflight still needs a bounded sandbox request for runtime behavior.

## Retries and Rate Limiting

The client retries only specific failures, and whether a failure qualifies depends on the operation. The rule that matters: **a write is never repeated unless the server said it was not processed, or you opted in.**

### What Is Retried

| Failure | Read operation | Write operation |
|---------|----------------|-----------------|
| `429 Too Many Requests` | Retried | Retried, because `429` means the request was not processed |
| `503 Service Unavailable` | Retried | **Not retried**: a `503` can arrive after the write committed |
| Other statuses, such as `500` or `502` | Not retried unless added to `retryOn` | Not retried, even if added to `retryOn` |
| Network error or timeout | Retried | **Not retried** |
| Caller abort through `AbortSignal` | Never | Never |

An operation counts as a **read** when its method is `GET` or `HEAD`, or when its operation ID starts with `list`, `count`, `search`, `exists` or `get`. Everything else counts as a write. That includes a raw `client.request({ method: 'POST', path })` call, even if it runs a list query, because there is no operation ID to classify it.

```js
// Customize the policy (these values are the defaults)
const client = createZeyosClient({
  platform: 'live',
  instance: 'demo',
  validate: true,
  retry: {
    maxRetries: 2,           // Retries after the first attempt
    retryOn: [429, 503],     // Candidate statuses; the table above still applies
    baseDelayMs: 300,        // Backoff base
    maxDelayMs: 10000,       // Cap for each wait
  },
});

// Disable retries entirely, including 429
const noRetry = createZeyosClient({ platform: 'live', instance: 'demo', retry: false });
```

### Delays

- **After a status response**, the client waits for the `Retry-After` header if it is present (seconds or an HTTP date). Without one, it uses exponential backoff with jitter: `baseDelayMs * 2^attempt`, plus a random amount up to `baseDelayMs`.
- **Every wait is capped at `maxDelayMs`.** A `Retry-After` longer than the cap is shortened, so a retry can arrive earlier than the server asked. If you must respect long rate-limit windows, raise `maxDelayMs` or handle the `429` in your own job.
- **After a network error or timeout**, the client always uses backoff, because there is no response to read.

Network failures and status retries share one `maxRetries` budget per request. Once it is spent, the last failure is thrown.

### Changing the Network Rule

`retryOnNetworkError` overrides the read/write classification. Omit it for automatic behavior, or set `true` or `false` for a client or a single request. The request value wins.

```js
await client.api.listTickets({ filters: { visibility: 0 } }, { retryOnNetworkError: false });
await client.api.updateTicket({ ID: 42, status: 4 }, { retryOnNetworkError: true }); // Same value each time
```

`true` also enables `503` and other `retryOn` statuses for that operation, not only network errors. Use it on a write only when repeating the request is harmless. An update that sets fixed values usually is, a create is not.

### Request Timeout and Job Budget

`timeoutMs` bounds **one attempt**, and each retry gets a fresh deadline. There is no default in JavaScript: without it, a stalled connection can wait forever, and the network retry never runs.

```js
// Per request
await client.api.listTickets({ filters: { visibility: 0 } }, { timeoutMs: 8000 });

// Or a client-wide default
const client = createZeyosClient({ platform: 'live', instance: 'demo', validate: true, timeoutMs: 8000 });
```

A timeout rejects with an `Error` where `isTimeout === true`, `code === 'ETIMEDOUT'` and `name === 'TimeoutError'`.

`timeoutMs` does not limit a whole call. One read can take `(maxRetries + 1) * timeoutMs` plus the waits, and a token refresh adds requests of its own. To limit a call or a job, pass an `AbortSignal`:

```js
const signal = AbortSignal.timeout(60_000); // Budget for everything below

for (const page of pages) {
  await client.api.listTickets(page, { signal, timeoutMs: 8000 });
}
```

Aborting your signal stops the current attempt or backoff wait immediately and is never retried. The error is your signal's abort reason, which has no `isTimeout` property, so you can tell it apart from a per-attempt timeout. Aborting stops the client from waiting. It does not undo a request the server already received.

### Uncertain Write Outcomes

When a write times out, loses its connection, is aborted or returns a `5xx`, **you do not know whether it happened**. A timed-out create may have succeeded. Retrying blindly can create a duplicate, so check first:

```js
import { ZeyosApiError, ZeyosValidationError, normalizeListResult } from '@zeyos/client';

function outcomeIsUncertain(err) {
  if (err instanceof ZeyosValidationError) return false; // Nothing was sent.
  if (err instanceof ZeyosApiError) return err.status >= 500;
  return true; // Timeout, abort or dropped connection.
}

async function findByExternalRef(externalRef) {
  const result = await client.api.listTickets({
    fields: ['ID'],
    filters: { 'extdata.external_ref': externalRef },
    limit: 2,
  });
  return normalizeListResult(result).data;
}

/** externalRef is your own stable ID, generated once per intended ticket. */
export async function createTicketOnce(externalRef, fields) {
  const input = { ...fields, extdata: { external_ref: externalRef } };
  try {
    return await client.api.createTicket(input);
  } catch (err) {
    if (!outcomeIsUncertain(err)) throw err;
    const matches = await findByExternalRef(externalRef); // If this fails, stay uncertain.
    if (matches.length > 1) throw new Error(`Duplicate tickets for ${externalRef}.`);
    if (matches.length === 1) return matches[0];
    throw err; // Not visible yet. Leave the decision to a later reconcile.
  }
}
```

This example needs `external_ref` in your `customFields` contract and on the instance. A request that timed out may still commit after your check, so "not found" is not proof of failure: rerun the check after a delay before creating again. If a resource has no field that can hold such a reference, match on business fields you control, and escalate to a person when the match is ambiguous.

Do not use retries for writes with external effects, such as sending a message. Reading the record back cannot prove what happened outside ZeyOS.

## Error Handling

All API errors are thrown as `ZeyosApiError` instances. This class extends `Error` and includes structured information about the failed request. When the server returns an error body with a message, a short snippet of it is folded into `err.message` (e.g. `api.listTickets failed with HTTP 400: unknown filter field: bogus`), so the thrown error says *why*, not just the status code. The full body is always on `err.body`.

```js
import { ZeyosApiError } from '@zeyos/client';

try {
  await client.api.getTicket({ ID: 999 });
} catch (err) {
  if (err instanceof ZeyosApiError) {
    console.log(err.status);       // 404
    console.log(err.statusText);   // 'Not Found'
    console.log(err.operationId);  // 'getTicket'
    console.log(err.service);      // 'api'
    console.log(err.method);       // 'GET'
    console.log(err.url);          // Full request URL
    console.log(err.body);         // Error response body (parsed JSON or text)
  }
}
```

Use these properties for debugging, but keep `err.body` and `err.message` out of production logs: they can contain record data from the response. The [server-side guide](https://www.zeyos.com/guide/tutorials/server-side-integrations#6-report-failures-without-leaking-data) shows a redacted log format.

The `ZeyosApiError` properties:

`JsonValue` means a parsed JSON scalar, array, or plain JSON object. Non-JSON error responses are returned as strings.

| Property | Type | Description |
|----------|------|-------------|
| `status` | `number` | HTTP status code (e.g. `404`, `401`, `500`) |
| `statusText` | `string` | HTTP status text (e.g. `'Not Found'`) |
| `operationId` | `string` | The operation that failed (e.g. `'getTicket'`) |
| `service` | `string` | The service key (e.g. `'api'`, `'oauth2'`) |
| `method` | `string` | HTTP method used (e.g. `'GET'`, `'POST'`) |
| `url` | `string` | The full request URL |
| `body` | `JsonValue` | The parsed JSON response body, plain-text error body, or `null` |
| `headers` | `Record<string,string>` | Response headers as a plain object |

### Unknown Operations

Calling an operation that does not exist rejects with a `ZeyosApiError` that suggests the closest match, instead of an opaque `is not a function` error -- useful when an operationId differs from the underlying table name:

```js
await client.api.listDunning({});
// ZeyosApiError: Unknown operation 'api.listDunning'. Did you mean 'listDunningNotices'?
```

### Validation Errors

When the client is created with `validate: true`, malformed requests reject with a `ZeyosValidationError` **before** any network call (see [Schema Introspection and Validation](#schema-introspection-and-validation)). It carries `operationId` and a structured `errors` array.

## Low-Level Requests

For endpoints not covered by the generated methods, or when you need full control over the request, use `client.request()`.

### By Operation ID

Reference a known operation by its service and operation ID:

```js
const result = await client.request({
  service: 'api',
  operationId: 'listTickets',
  body: { filters: { status: 1, visibility: 0 }, limit: 10 },
});
```

### By Path and Method

Specify the HTTP method and path directly for custom or undocumented endpoints. This skips schema validation and read classification, so a `POST` list sent this way is not retried after a network error, a timeout or a `503`:

```js
const result = await client.request({
  service: 'api',
  method: 'POST',
  path: '/tickets/',
  body: { filters: { status: 1, visibility: 0 } },
});
```

### Raw Responses

Pass `raw: true` to receive the full response envelope instead of just the parsed body:

```js
const response = await client.request({
  service: 'api',
  operationId: 'listTickets',
  body: { limit: 10 },
  raw: true,
});

console.log(response.status);     // 200
console.log(response.headers);    // Response headers
console.log(response.data);       // Parsed body
```

## Request Options

All generated methods and `client.request()` accept an optional second argument with request-level options:

| Option | Type | Description |
|--------|------|-------------|
| `signal` | `AbortSignal` | An `AbortController` signal to cancel the request |
| `timeoutMs` | `number` | Abort each attempt after N ms (composes with `signal`); also settable client-wide as `timeoutMs` |
| `retryOnNetworkError` | `boolean` | Force or disable retrying network errors, timeouts and `retryOn` statuses other than `429` for this call (default: reads only) |
| `validate` | `boolean` | Pass `true` to validate this call even if the client has validation off |
| `raw` | `boolean` | Return the full response envelope instead of just the data |
| `auth` | `string \| { mode?: string, accessToken?: string, access_token?: string, refreshToken?: string, refresh_token?: string, clientId?: string, client_id?: string, clientSecret?: string, client_secret?: string }` | Override the authentication mode or credentials for this request |
| `baseUrl` | `string` | Override the base URL for this request |
| `bodyType` | `'json' \| 'form'` | Force a body encoding |

Example with an abort controller:

```js
const controller = new AbortController();

// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);

const tickets = await client.api.listTickets(
  { limit: 100 },
  { signal: controller.signal }
);
```

:::tip Persisting refreshed tokens
When using token mode with `autoRefresh` in a trusted environment, tokens are updated in the token store automatically. If you use a `MemoryTokenStore`, those refreshed tokens will be lost on page reload. Use the `syncTokens` pattern to persist them:

```js
async function syncTokens() {
  const tokenSet = await client.auth.getTokenSet();
  if (tokenSet?.accessToken) {
    localStorage.setItem('zeyos_tokens', JSON.stringify(tokenSet));
  }
}

// Call after important API operations
const tickets = await client.api.listTickets({ limit: 50 });
await syncTokens();
```
:::

---

Canonical: https://www.zeyos.com/guide/javascript-client/practical-guide

# Practical Implementation Guide

This guide documents patterns and gotchas discovered while building a real application on top of the ZeyOS JavaScript client. It supplements the reference documentation with things that only become apparent once you start making actual API calls.

Create clients with `validate: true`. Many of the mistakes below are then rejected before a request is sent, with a hint naming the correct field. The examples assume that setting.

## HTTP Method Conventions

ZeyOS uses an unconventional but consistent REST convention that often surprises developers:

| Operation | HTTP Method | Notes |
|-----------|-------------|-------|
| List records | **POST** | Query params (filters, sort, fields) go in the request body |
| Get a record | **GET** | |
| Create a record | **PUT** | Not POST — ZeyOS uses PUT for creation |
| Update a record | **PATCH** | Partial updates; only send fields you want to change |
| Delete a record | **DELETE** | |
| Check existence | **HEAD** | Returns `true` (no body) on success |

The most important one to internalise: **list operations are POST requests**. This makes sense once you consider that complex queries with nested filters would quickly exceed URL length limits as query strings.

## Passing the Request Body for Update Operations

Generated methods accept the natural flat style for operations that have both a path parameter and a request body:

```js
await client.api.updateTicket({ ID: 42, status: 4, priority: 2 });
```

The client routes known path/query/header parameters to the request URL and sends the remaining non-reserved keys as the body. Explicit `body` and `data` keys are still supported when you want to separate those concerns yourself:

```js
await client.api.updateTicket({ ID: 42, body: { status: 4, priority: 2 } });
await client.api.updateTask({ ID: taskId, data: { name: 'New name', duedate: ts } });
```

For low-level `client.request()` calls, prefer explicit `body` because there is no generated operation metadata to tell the client which keys are URL parameters. Pass `operationId` when one exists: a method and path call also skips validation, so `validate: true` will not catch a wrong field name there.

## Always use `filters` (plural)

There is one filter parameter, and it is `filters`. Use it for every field type — scalars,
enums, and GIN-indexed foreign keys alike.

The singular `filter` appears once in the bundled OpenAPI spec and nowhere in ZeyOS's
[published API documentation](https://www.zeyos.com/api/llms-full.txt), whose worked examples
use `filters` throughout. Treat the spec as the outlier. The CLI's schema validation rejects
`filter` outright and tells you to use `filters`.

```js
// Scalars, enums and foreign keys all go in the same place
const tickets = await client.api.listTickets({
  filters: { visibility: 0, project: projectId },
  sort: ['-lastmodified'],
  limit: 500,
});

// A foreign key is no different
const tasks = await client.api.listTasks({
  fields: ['ID', 'tasknum', 'name', 'duedate', 'assigneduser'],
  filters: { ticket: ticketId, visibility: 0 },
  sort: ['+name'],
  limit: 200,
});
```

:::tip
Use `filters` for everything. If you pass `filter` with validation enabled, the client rejects it and names the correct key.
:::

## Use `visibility: 0` — where the column exists

Many ZeyOS records have a `visibility` field controlling soft-deletion and archiving, where
`0` = regular, `1` = archived, `2` = deleted. Records with `visibility > 0` are hidden from
normal views, so include `visibility: 0` unless you intentionally want them:

```js
const filters = { visibility: 0 };
// Add resource-specific filters after
if (projectId) filters.project = projectId;
```

:::warning Not every resource has the column
`visibility` exists on `accounts`, `contacts`, `tickets`, `tasks`, `projects`, `items`,
`documents`, `notes`, `opportunities`, `appointments`, `campaigns`, `mailinglists`,
`storages`, `contracts` and `ledgers`.

It does **not** exist on `transactions` — nor any billing/procurement entity derived from it —
nor on `payments`, `messages`, `actionsteps`, `addresses`, `users`, `prices` or `dunning`.
Filtering on a column a resource lacks returns an **opaque HTTP 400** that does not name the
offending field.

Check with `client.schema.describe('<resource>')` or `zeyos describe <entity>` when unsure.
:::

## Normalising List Responses

List operations are not perfectly uniform across the whole surface area. Use the shared helper so every call site follows the same response-shape handling:

```js
import { normalizeListResult } from '@zeyos/client';

const result = await client.api.listTickets({ filters: { visibility: 0 } });
const { data: tickets } = normalizeListResult(result);
```

Use `normalizeCountResult()` for count-only requests.

## Date and Timestamp Handling

ZeyOS stores all dates as **Unix timestamps in seconds** (not milliseconds). When reading:

```js
// Convert to a JavaScript Date
const date = new Date(ticket.duedate * 1000);

// Format for display
const label = new Date(ticket.duedate * 1000).toLocaleDateString(undefined, {
  month: 'short', day: 'numeric', year: 'numeric',
});

// Check if overdue
const isOverdue = ticket.duedate * 1000 < Date.now();
```

When writing (e.g. from an HTML `<input type="date">`):

```js
const dueDateVal = form.querySelector('#due-date').value; // '2026-03-15'
const duedate = dueDateVal
  ? Math.floor(new Date(dueDateVal).getTime() / 1000)
  : null;

await client.api.updateTicket({ ID: id, body: { duedate } });
```

## Selecting Fields for Performance

Always pass a `fields` array in list requests. Without it, every field on every record is returned, which can significantly increase payload size and response time:

```js
// ✗ Returns all fields for every ticket
const tickets = await client.api.listTickets({ limit: 500 });

// ✓ Returns only what you need
const tickets = await client.api.listTickets({
  fields: ['ID', 'ticketnum', 'name', 'status', 'priority', 'duedate'],
  filters: { visibility: 0 },
  limit: 500,
});
```

For single-record GET operations, field selection is not available — all standard fields are returned. Use query flags like `extdata: 1` and `tags: 1` to opt into additional data:

```js
const ticket = await client.api.getTicket({ ID: id, extdata: 1, tags: 1 });
```

## Optimistic UI Updates with Server Verification

For immediate feedback on user actions (like drag-and-drop), apply the change to local state first, then confirm with the server and revert if it fails. Use the response body to confirm the actual resulting value:

```js
const fromStatus = ticket.status;

// 1. Optimistic update — instant visual feedback
ticket.status = toStatus;
updateColumn(fromStatus);
updateColumn(toStatus);

try {
  // 2. Send PATCH — response body contains the updated record
  const updated = await client.api.updateTicket({
    ID: ticket.ID,
    body: { status: toStatus },
  });

  // 3. Confirm — use the server's value in case it was clamped or rejected
  const confirmedStatus = updated?.status ?? toStatus;
  if (confirmedStatus !== toStatus) {
    ticket.status = confirmedStatus;
    updateColumn(toStatus);
    updateColumn(confirmedStatus);
  }
} catch (err) {
  // 4. Revert on failure
  ticket.status = fromStatus;
  updateColumn(fromStatus);
  updateColumn(toStatus);
  showError(`Move failed: ${err.message}`);
}
```

## Persisting Refreshed Tokens

When using token mode with `autoRefresh: true` in a trusted environment, the client silently refreshes expired access tokens. The refreshed tokens are stored in the `MemoryTokenStore` but lost on page reload unless you persist them explicitly. Call a sync function after important API operations:

```js
async function syncTokens() {
  try {
    const ts = await client.auth.getTokenSet();
    if (ts?.accessToken) {
      localStorage.setItem('zeyos_tokens', JSON.stringify({
        accessToken:           ts.accessToken,
        refreshToken:          ts.refreshToken,
        expiresAt:             ts.expiresAt,
        refreshTokenExpiresAt: ts.refreshTokenExpiresAt,
      }));
    }
  } catch {
    // Non-critical — silently ignore
  }
}

// Usage
const tickets = await client.api.listTickets({ filters: { visibility: 0 } });
await syncTokens(); // Persist any refreshed tokens
```

## Session Detection Without Tokens

If you don't have an OAuth token but the user is already logged into ZeyOS in the same browser, you can detect their session via the userinfo endpoint:

```js
async function trySessionAuth(instanceUrl) {
  try {
    const res = await fetch(`${instanceUrl}oauth2/v1/userinfo`, {
      credentials: 'include',
    });
    if (res.ok) return await res.json();
  } catch {
    // No session
  }
  return null;
}

const userInfo = await trySessionAuth('https://cloud.zeyos.com/demo/');
if (userInfo) {
  // Session is active — initialize in session mode
  const client = createZeyosClient({
    platform: instanceUrl,
    validate: true,
    auth: { mode: 'session', session: { enabled: true, credentials: 'include' } },
  });
}
```

:::note
Session mode requires that your app is served from the same origin as ZeyOS, or that the ZeyOS instance is configured to allow cross-origin requests with credentials. If you are on a different domain, token mode is more reliable.
:::

## Navigating to ZeyOS Views

To link users directly to a record inside the ZeyOS web interface, construct a URL in this format:

```
<INSTANCE_URL>?umi=<MODULE>&page=<PAGE>&id=<RECORD_ID>&tab=<TAB>
```

Common examples:

```js
const baseUrl = 'https://cloud.zeyos.com/demo/';

// Link to a ticket
`${baseUrl}?umi=tickets&page=details_ticket&id=${ticketId}&tab=0`

// Link to a task (within the tickets module)
`${baseUrl}?umi=tickets&page=details_ticket&id=${taskId}&tab=0`

// Link to an account
`${baseUrl}?umi=accounts&page=details_account&id=${accountId}&tab=0`
```

## Extended Data (extdata)

Many ZeyOS entities support custom fields via `extdata`. These are returned as a nested object.

Keys differ per instance. With validation on, naming a key requires a reviewed [`customFields` contract](https://www.zeyos.com/guide/javascript-client/making-requests#custom-field-contracts) for that instance, so the first example below needs `region` and `customer_type` in the contract for `tickets`. Do not turn validation off to get past an unknown key: resolve the key from the instance's `customfields` definitions instead. `extdata: 1` reads the whole collection without a contract.

```js
// Request extended data in a list
const tickets = await client.api.listTickets({
  fields: ['ID', 'name', 'extdata.region', 'extdata.customer_type'],
  filters: { visibility: 0 },
});

// Or include all extdata for single-record fetches
const ticket = await client.api.getTicket({ ID: id, extdata: 1 });
console.log(ticket.extdata); // { region: 'EMEA', customer_type: 'Enterprise', ... }
```

When saving extended data back, pass it as a plain object:

```js
await client.api.updateTicket({
  ID: id,
  body: {
    extdata: { region: 'APAC', customer_type: 'SMB' },
  },
});
```

## Common Status and Priority Values

Ticket and task status and priority values are plain integers. The canonical values observed in the ZeyOS API:

### Ticket Status

| Value | Label |
|-------|-------|
| `0` | Not Started |
| `1` | Awaiting Acceptance |
| `2` | Accepted |
| `3` | Rejected |
| `4` | Active |
| `5` | Inactive |
| `6` | Feedback Required |
| `7` | Testing |
| `8` | Cancelled |
| `9` | Completed |
| `10` | Failed |
| `11` | Booked |

### Ticket Priority

| Value | Label |
|-------|-------|
| `0` | Lowest |
| `1` | Low |
| `2` | Medium |
| `3` | High |
| `4` | Highest |

## Error Handling Checklist

`ZeyosApiError` is thrown for all non-2xx responses. Key properties to check:

```js
import { ZeyosApiError } from '@zeyos/client';

try {
  await client.api.updateTicket({ ID: id, body: data });
} catch (err) {
  if (!(err instanceof ZeyosApiError)) throw err; // Re-throw unexpected errors

  if (err.status === 401) {
    // Session expired or token invalid — redirect to login
  } else if (err.status === 403) {
    // Insufficient permissions
  } else if (err.status === 404) {
    // Record does not exist
  } else if (err.status === 409) {
    // Conflict — record was modified since last read (check If-Match header usage)
  } else {
    // Generic error. Log the category, not err.body: it can contain record data.
    console.error(err.status, err.operationId);
  }
}
```

A thrown error does not always mean a write failed. After a timeout, a dropped connection or a `5xx`, the write may have succeeded, so [reconcile before retrying](https://www.zeyos.com/guide/javascript-client/making-requests#uncertain-write-outcomes).

:::tip
On 401, the client automatically retries with a refreshed token if `autoRefresh: true` is set, a refresh token is available, and OAuth client credentials are configured. You will only see a 401 error if the refresh also fails — typically meaning the user's session has fully expired.
:::

---

Canonical: https://www.zeyos.com/guide/okf/keeping-fresh

# Keeping OKF Fresh

The OKF bundle is generated from the OpenAPI/dbref snapshots in `openapi/`. When ZeyOS
updates its database model or API, those specs are re-exported and the bundle is
regenerated. The design keeps that safe and observable.

## What protects curation

1. **Deterministic generation.** `npm run generate` runs the OKF producer alongside the
   client codegen. Re-running with unchanged specs produces **no diff** (no timestamp churn).
2. **Managed blocks.** Only the fenced `okf:generated` region of each entity concept is
   rewritten; curated `# Notes`/`# Metrics` prose is spliced back unchanged. See the
   [overview](https://www.zeyos.com/guide/okf/overview#generated-vs-curated-managed-blocks).
3. **`source_snapshot`.** The root `index.md` frontmatter carries a content hash of the
   schema. When the specs change, the hash changes — a staleness signal for consumers and
   the drift gate.
4. **`log.md` schema diff.** The producer diffs the new schema against the last snapshot and
   appends a dated entry (added/removed fields, changed enums, new foreign keys) — an
   OKF-native, human- and agent-readable record of *what changed when the model updated*.

## The spec-refresh runbook

```bash
# 1. Replace the snapshots with the newly exported specs
#    openapi/api.json, openapi/dbref.json

# 2. Regenerate the client and the OKF bundle
npm run generate

# 3. Review the diff and the changelog
git diff okf/ agents/shared/zeyos-entity-reference.md
cat okf/log.md          # newest dated entry summarizes the schema changes

# 4. Validate conformance + the drift gate
npm run okf:check

# 5. (Optional) enrich weak concepts — see ./04-loops.md
npm run okf:refine -- --concept entities/<changed-entity>

# 6. Commit
git add okf agents/shared src/generated && git commit -m "Refresh schema + OKF"
```

## The drift gate (CI)

`npm run okf:check` runs `test/okf.test.js`, which regenerates the bundle and asserts the
committed content matches a fresh regen. It fails the build when:

- the specs changed but `okf/` wasn't regenerated and committed;
- someone hand-edited a generated region;
- a concept is non-conformant, an API-backed entity lacks a concept, or a structural
  cross-link is broken;
- the shared reference's generated operationId table fell out of sync.

---

Canonical: https://www.zeyos.com/guide/okf/loops

# Refining with Loops

OKF and the skill pack are refined with the same loop machinery the agent protocol already
uses — a measurement loop and a refinement loop that close on each other.

## Measure: OKF as agent context

The agent protocol (`npm run test:agent-protocol`) drives a real coding agent through
business scenarios against a live demo instance and verifies each outcome independently. A
`--context` axis chooses which knowledge the agent is pointed at:

```bash
# Skills only (default), OKF only, or both
npm run test:agent-protocol -- --context okf  --scenario b03-billing-transaction-count
npm run test:agent-protocol -- --context both --layer b
```

The bundle is exposed to the agent as `ZEYOS_OKF_ROOT` (mirroring `ZEYOS_SKILL_ROOT`). The
loop runner sweeps the axis and reports per-context pass rates, so you can see whether
OKF-as-context lifts accuracy and which concepts correlate with failures:

```bash
npm run test:agent-loop -- --context skills,okf,both --read-only --agents opencode
```

The scorecard tells you which **skill** and which **OKF concept** to improve.

## Refine: generate → validate → judge → apply

`npm run okf:refine` improves a concept's **curated** guidance (never the generated block):

```bash
# Target a concept directly, or derive weak concepts from a run's scorecard
npm run okf:refine -- --concept entities/tickets
npm run okf:refine -- --scorecard test/agent-protocol/results/<run>/scorecard.json
npm run okf:refine -- --concept entities/transactions --apply   # write the accepted revision
```

Each target goes through:

1. **Generate** — a proposer model drafts improved curated notes from the current concept.
2. **Validate** — any field the proposal references must exist on the entity (checked
   against the client schema), so the model can't invent columns or enums.
3. **Judge** — a held-out judge model (`agentProtocol.judgeModel`, reusing `judge.mjs`)
   approves only if the revision is more accurate and useful and contradicts no schema fact.
4. **Apply** — with `--apply`, the accepted notes replace the curated tail; the generated
   managed block is never touched. Without `--apply`, proposals are written for review.

## The closed loop

```
specs + curation ──▶ okf/ ──▶ agent-protocol (--context okf) ──▶ scorecard
        ▲                                                          │
        └──────────── okf:refine (curated notes) ◀─────────────────┘
```

Drive it self-paced with the Claude Code `/loop` skill: *measure → pick the weakest concept
→ refine → re-measure*, until pass rates plateau.

---

Canonical: https://www.zeyos.com/guide/okf/overview

# OKF Overview

The **Open Knowledge Format (OKF v0.1)** is Google's minimal, vendor-neutral spec for
sharing the metadata and curated context that surrounds data: a directory of Markdown
files, each with YAML frontmatter whose only required field is `type`, optional `index.md`
and `log.md`, and Markdown cross-links that turn the directory into a knowledge graph.
Producers emit a bundle; consumers (coding agents, viewers, search) read it.

`@zeyos/client` ships a conformant OKF bundle under [`okf/`](https://github.com/zeyos/client/tree/main/okf)
that describes the ZeyOS data model, and tooling to produce, consume, validate, and refine it.

## Why OKF fits ZeyOS

The client already derives a compact schema from the OpenAPI/dbref specs. OKF turns that —
plus the curated business knowledge that used to live only in the skill pack — into a
portable knowledge layer any agent or tool can read, independent of this client.

The bundle is **canonical** for ZeyOS structural facts: the hand-maintained
`agents/shared/zeyos-entity-reference.md` operationId table is now generated from the same
source, so the skills and the OKF bundle can't drift apart.

## Bundle layout

```
okf/
  index.md                  # root listing; frontmatter: okf_version, source_snapshot
  log.md                    # schema-change history (auto-appended on real changes)
  entities/                 # one concept per API-backed entity — type: ZeyOS Entity
    index.md
    accounts.md  tickets.md  transactions.md  …
  metrics/                  # business metric definitions — type: Metric
  playbooks/                # step-by-step query workflows — type: Playbook
  concepts/                 # cross-cutting rules / footguns — type: Reference
```

Each entity concept carries a **Schema** table (column, type, nullability, default, index,
foreign key), **Foreign Keys** (cross-linked to the target entity), **Enums** (parsed from
the schema), **Indexes** (including the GIN/partial indexes behind the `filters` footgun),
and **Operations** (the real `@zeyos/client` operationIds, read straight from `api.json`).

## Generated vs curated: managed blocks

Each entity concept mixes auto-generated structure with curated prose. The generated region
is fenced by HTML-comment markers:

```markdown
<!-- okf:generated:start — rewritten by scripts/generate-okf.mjs; do not edit by hand -->
# Schema
…
<!-- okf:generated:end -->

# Notes
Curated guidance — preserved across every regeneration.
```

The producer rewrites **only** the region between the markers. Curated `# Notes`/`# Metrics`
prose (added by a human or the refinement loop) is preserved verbatim, so regenerating after
a schema change never clobbers curation. See [Keeping OKF fresh](https://www.zeyos.com/guide/okf/keeping-fresh).

## Relationship to the skill pack

Two projections of one knowledge core:

- **Skills** (`agents/`) stay the task/runner-facing layer: the operating contract, "act,
  don't plan", and safety.
- **OKF** (`okf/`) is the reference layer: the entity schema-of-record plus the metric,
  playbook, and concept catalog.

Skills point into `okf/`; when a schema fact in a shared reference and in `okf/` ever
disagree, `okf/` wins.

---

Canonical: https://www.zeyos.com/guide/okf/producing-and-consuming

# Producing & Consuming OKF

## CLI

```bash
zeyos okf list                 # list concepts (type, id, title); --json for automation
zeyos okf show tickets         # print a concept (bare resource name or entities/tickets)
zeyos okf check                # validate the bundle for OKF v0.1 conformance (exit non-zero on error)
zeyos okf export --out ./okf   # copy the shipped bundle into a directory
zeyos okf build  --out ./okf   # synthesize a bundle from the client's schema
```

`export` copies the rich shipped bundle (with curated metrics/playbooks/notes). `build`
synthesizes a structural bundle from the client's introspection surface — useful where the
shipped files aren't present, or to diff against a live instance.

## JavaScript client

The OKF surface is exported from `@zeyos/client`:

```js
import { buildOkf, loadOkfBundle, validateOkfBundle, validateOkfFiles, OKF_VERSION } from '@zeyos/client';

// Synthesize a conformant bundle from the client's generated schema (pure; browser-safe).
const files = buildOkf();                       // { 'entities/tickets.md': '…', … }
const { valid, errors } = validateOkfFiles(files);

// Load the shipped (or any) bundle from disk (Node only).
const bundle = await loadOkfBundle('node_modules/@zeyos/client/okf');
bundle.version;                                 // '0.1'
bundle.concepts['entities/tickets'].frontmatter // { type: 'ZeyOS Entity', title: 'Tickets', … }

// Validate a directory or an in-memory file map.
await validateOkfBundle('okf');
```

- `buildOkf({ schema?, services? })` — pure producer; defaults to the generated `SCHEMA`/`SERVICES`.
- `loadOkfBundle(dir)` — Node-only reader (lazy `fs`), returns `{ version, files, concepts }`.
- `validateOkfBundle(dirOrFiles)` / `validateOkfFiles(files)` — v0.1 conformance check.

## Build-time producer

In this repo, `npm run okf:build` (or `npm run generate`, which runs it alongside the client
codegen) emits the rich bundle from `openapi/{api,dbref}.json` plus the curated content in
`scripts/data/okf-curation.mjs`. It also injects the generated operationId table into
`agents/shared/zeyos-entity-reference.md`. Validate with `npm run okf:check`.

---

Canonical: https://www.zeyos.com/guide/sample-apps/crm

# CRM Account List

The `samples/crm/` application shows how to build a read-heavy account UI with server-side search, joined contact fields, modal editing, and predictable pagination.

## Problem Solved

Use this sample when you need a compact CRUD interface for operational data such as accounts or contacts and you want the server, not the browser, to do the filtering and sorting work.

## Auth Model

The sample uses the same dual browser model as the Kanban app:

- **Token mode** with a pre-obtained access token and optional stored refresh token
- **Session mode** with a `/oauth2/v1/userinfo` probe

Configuration can come from `data-zeyos-*` attributes, `localStorage`, or the `window.ZeyOS` console API.

For long-lived browser sessions, prefer session mode or move OAuth refresh to a backend. Session mode only works from the same origin or when the ZeyOS instance allows credentialed CORS, so token mode is usually the local-development path. The sample keeps browser code free of client credentials.

## Main API Calls

| Operation | Usage in the sample |
|-----------|---------------------|
| `listAccounts` | Fetch paginated CRM rows with aliased and joined fields |
| `getAccount` | Load the full record before editing |
| `createAccount` | Create new accounts from the modal |
| `updateAccount` | Apply modal edits via `{ ID, body: data }` |
| `deleteAccount` | Remove records from the dataset |

## Reusable Patterns

- **Object-form `fields`**: the UI requests aliased fields such as `City: 'contact.city'`
- **Server-side search**: full-text queries use the `query` parameter instead of client-side filtering
- **Server-side sorting**: column headers map friendly names back to raw API fields
- **Update helpers**: update helpers use `{ ID, body: data }` to keep path parameters separate from changed fields
- **Token persistence**: runtime tokens are stored in `localStorage` for the next page load

## Safe to Copy

The sample is a strong reference for:

- dot-notation joins with aliased output fields
- building sort maps from UI column names to API field paths
- debounced search that resets pagination
- small browser apps that still keep auth, state, API, and UI concerns separate

When copying code outside this repository, replace source-tree imports such as `../../../src/index.js` with an import path that exists in your app: an npm package import, a vendored copy of `src/`, or a local symlink.

## Run Locally

```bash
cd /path/to/zeyos/client
python3 -m http.server 8080
```

Open:

```text
http://localhost:8080/samples/crm/
```

## Console API

| Method | Description |
|--------|-------------|
| `ZeyOS.setUrl(url)` | Set the ZeyOS instance URL |
| `ZeyOS.setToken(access, refresh?)` | Persist access and optional refresh tokens |
| `ZeyOS.status()` | Print connection state, search, sort, and page info |
| `ZeyOS.logout()` | Clear stored config and reload |
| `ZeyOS.reconnect()` | Reload and re-run the auth boot sequence |

## What to Read Next

- [Browser UI Playbook](https://www.zeyos.com/guide/tutorials/build-your-own-zeyos-frontend)
- [Making Requests](https://www.zeyos.com/guide/javascript-client/making-requests)
- [CRM sample README](https://www.zeyos.com/guide/sample-apps/crm)

---

Canonical: https://www.zeyos.com/guide/sample-apps/dashboard

# Dashboard

The `samples/dashboard/` application shows how to build a read-only operational dashboard that combines multiple ZeyOS queries into one browser view.

## Problem Solved

Use this sample when you need KPI cards, summary charts, and recent-activity panels without building a heavy frontend stack.

## Auth Model

The dashboard follows the same browser auth model as the other samples:

- **Token mode** with a pre-obtained access token and optional stored refresh token
- **Session mode** via `/oauth2/v1/userinfo`

Configuration can come from `data-zeyos-*` attributes, `localStorage`, or the `window.ZeyOS` console API.

For long-lived browser sessions, prefer session mode or move OAuth refresh to a backend. Session mode only works from the same origin or when the ZeyOS instance allows credentialed CORS, so token mode is usually the local-development path. The sample avoids shipping client credentials in browser code.

## Main API Calls

| Operation | Usage in the sample |
|-----------|---------------------|
| `listTickets` with `count: true` | Build total, active, and overdue KPI cards |
| `listTickets` with explicit fields | Render recent ticket activity |
| `listAccounts` with `count: true` | Build total account KPIs |
| `listAccounts` with joins | Render recent account activity |

## Reusable Patterns

- **Parallel data loading**: the dashboard fires independent queries with `Promise.all`
- **Count-first dashboards**: KPI cards use count-enabled requests instead of loading full datasets
- **Defensive count handling**: the sample treats count-enabled responses conservatively instead of assuming a single wrapper shape
- **Read-only browser views**: the app demonstrates a useful UI that never mutates records
- **Minimal field selection**: each summary query requests only the fields needed for display

## Safe to Copy

The dashboard is a good template for:

- status or backlog overview pages
- home pages for internal operator portals
- management dashboards that aggregate a few targeted ZeyOS queries
- small browser apps that need strong perceived performance from parallel loading

When copying code outside this repository, replace source-tree imports such as `../../../src/index.js` with an import path that exists in your app: an npm package import, a vendored copy of `src/`, or a local symlink.

## Run Locally

```bash
cd /path/to/zeyos/client
python3 -m http.server 8080
```

Open:

```text
http://localhost:8080/samples/dashboard/
```

## Console API

| Method | Description |
|--------|-------------|
| `ZeyOS.setUrl(url)` | Set the ZeyOS instance URL |
| `ZeyOS.setToken(access, refresh?)` | Persist access and optional refresh tokens |
| `ZeyOS.status()` | Print effective connection state |
| `ZeyOS.logout()` | Clear stored config and reload |
| `ZeyOS.reconnect()` | Reload and re-run the auth boot sequence |

## What to Read Next

- [Server-Side Integrations](https://www.zeyos.com/guide/tutorials/server-side-integrations)
- [Making Requests](https://www.zeyos.com/guide/javascript-client/making-requests)
- [Dashboard sample README](https://www.zeyos.com/guide/sample-apps/dashboard)

---

Canonical: https://www.zeyos.com/guide/sample-apps/kanban

# Kanban Board

The `samples/kanban/` application shows how to build a browser-based work queue on top of ZeyOS using plain ES modules and the generated JavaScript client.

## Problem Solved

Use this sample when you need a UI for operators who move work through statuses, inspect details, and create follow-up work without leaving a lightweight browser app.

## Auth Model

The sample supports both browser auth models used throughout this docs set:

- **Token mode** using a pre-obtained access token and optional stored refresh token
- **Session mode** by probing `/oauth2/v1/userinfo` with `credentials: 'include'`

Configuration can come from:

- `data-zeyos-*` attributes in `samples/kanban/index.html`
- persisted `localStorage` values
- the `window.ZeyOS` console API

For long-lived browser sessions, prefer session mode or move OAuth refresh to a backend. Session mode only works from the same origin or when the ZeyOS instance allows credentialed CORS, so token mode is usually the local-development path. The sample does not embed client credentials for browser-side refresh.

## Main API Calls

| Operation | Usage in the sample |
|-----------|---------------------|
| `listTickets` | Load the board columns and context-filtered ticket lists |
| `getTicket` | Fetch the full ticket for the detail dialog |
| `createTicket` | Create new tickets from the modal |
| `updateTicket` | Move tickets between columns and edit ticket details |
| `deleteTicket` | Remove tickets from the board |
| `listTasks` | Load tasks linked to a ticket |
| `listProjects` | Populate the project context dropdown |

## Reusable Patterns

- **Auth boot sequence**: URL resolution, token detection, session probe, then app boot
- **Optimistic UI update**: move the card first, then confirm with `updateTicket`, then revert on failure
- **Explicit update body**: all ticket updates in this sample use `{ ID, body }` for clarity (the flat spread form `{ ID, status }` also works)
- **Context filtering**: a single filter object drives board views such as “all tickets” or “project tickets”
- **Persistent UI settings**: board columns and runtime config live in `localStorage`

## Safe to Copy

The following pieces are good starting points for a new UI:

- the token/session boot sequence in `samples/kanban/js/main.js`
- the session detection helper in `samples/kanban/js/auth.js`
- the optimistic update pattern around `updateTicket`
- the `window.ZeyOS` console API for local development and troubleshooting

When copying code outside this repository, replace source-tree imports such as `../../../src/index.js` with an import path that exists in your app: an npm package import, a vendored copy of `src/`, or a local symlink.

## Run Locally

Serve the repository root with any static file server:

```bash
cd /path/to/zeyos/client
python3 -m http.server 8080
```

Then open:

```text
http://localhost:8080/samples/kanban/
```

## Console API

| Method | Description |
|--------|-------------|
| `ZeyOS.setUrl(url)` | Set the ZeyOS instance URL |
| `ZeyOS.setToken(access, refresh?)` | Persist access and optional refresh tokens |
| `ZeyOS.status()` | Print effective connection state |
| `ZeyOS.logout()` | Clear stored config and reload |
| `ZeyOS.reconnect()` | Reload and re-run the auth boot sequence |

## What to Read Next

- [Browser UI Playbook](https://www.zeyos.com/guide/tutorials/build-your-own-zeyos-frontend)
- [Practical Guide](https://www.zeyos.com/guide/javascript-client/practical-guide)
- [Kanban sample README](https://www.zeyos.com/guide/sample-apps/kanban)

---

Canonical: https://www.zeyos.com/guide/tutorials/application-developers

# Application Developers

This path is for developers building external applications that use ZeyOS as the central source of business data and business logic.

For v1, the scope is explicitly **external integrations**:

- browser UIs
- server-side services
- automation backends
- connected internal tools

These docs do **not** cover authoring native ZeyOS platform artifacts.

## Choose Your Starting Point

| Goal | Start here | Why |
|------|------------|-----|
| Decide between browser, token, and server architectures | [Integration Architecture](https://www.zeyos.com/guide/tutorials/integration-architecture) | Compares auth and deployment models before you write code |
| Build a browser UI or internal tool | [Browser UI Playbook](https://www.zeyos.com/guide/tutorials/build-your-own-zeyos-frontend) | Covers session mode, controlled browser token mode, list queries, CRUD, and UI-safe patterns |
| Build a backend, worker, or scheduled integration | [Server-Side Integrations](https://www.zeyos.com/guide/tutorials/server-side-integrations) | Covers token storage, refresh, low-level requests, and sync jobs |
| Reuse working implementation patterns | [Sample Applications](https://www.zeyos.com/guide/sample-apps/kanban) | Shows reusable frontend patterns without framework lock-in |
| Need the full generated surface | [JavaScript Client](https://www.zeyos.com/guide/javascript-client/getting-started) | Full API coverage and low-level request escape hatch |

## Recommended Defaults

- Prefer `@zeyos/client` for JavaScript applications.
- Use `filters` in client code unless you have a reason to stay with raw API `filter`.
- Use `visibility: 0` on resources that **have** the column — `accounts`, `contacts`, `tickets`, `tasks`, `projects`, `items`, `documents`, `notes`, `opportunities`, `appointments`, `campaigns`, `mailinglists`, `storages`, `contracts`, `ledgers` — unless you want archived (`1`) or deleted (`2`) records. **Most other resources have no `visibility` column** — `transactions` (and every billing/procurement entity), `payments`, `messages`, `actionsteps`, `addresses`, `users`, `prices`, `dunning` — and filtering on it there returns an opaque HTTP 400. Check with `zeyos describe <entity>` when unsure.
- Use explicit `body` objects for update operations that also include path parameters such as `ID`.
- Treat `extdata` and `expand` as separate concepts:
  - `extdata` exposes custom fields
  - `expand` inlines JSON or binary columns

## Next Steps

- Read [Integration Architecture](https://www.zeyos.com/guide/tutorials/integration-architecture)
- Pick either the [Browser UI Playbook](https://www.zeyos.com/guide/tutorials/build-your-own-zeyos-frontend) or [Server-Side Integrations](https://www.zeyos.com/guide/tutorials/server-side-integrations)
- Use the [API Reference](https://www.zeyos.com/guide/api-reference/data-retrieval) and [JavaScript Client docs](https://www.zeyos.com/guide/javascript-client/making-requests) as the detailed reference layers

---

Canonical: https://www.zeyos.com/guide/tutorials/build-your-own-zeyos-frontend

# Browser UI Playbook

This playbook walks through building a browser-based interface on top of the ZeyOS REST API using the `@zeyos/client` JavaScript library. By the end, you will have a working single-page application that authenticates, queries data, renders a UI, and writes changes back with patterns you can reuse in any ZeyOS-connected browser app.

We will build a minimal **Ticket Dashboard** step by step. No application framework or build step is required -- just ES modules, the ZeyOS client, and a browser. The starter HTML below uses the Tailwind CDN only for concise demo styling; remove it or self-host your CSS for production or stricter security environments.

---

## Prerequisites

- A **ZeyOS instance** with some ticket data (e.g. `https://cloud.zeyos.com/demo/`)
- An **access token** (obtain one via the [CLI](https://www.zeyos.com/guide/cli/getting-started) or the ZeyOS OAuth2 flow)
- A **local HTTP server** to serve static files (e.g. `python3 -m http.server 8080` or `npx serve .`)

---

## Step 1: Project Setup

Create a project folder with this structure:

```
my-zeyos-app/
  index.html
  app.js
  zeyos-client/        # symlink or copy of the @zeyos/client src/ directory
```

Link the ZeyOS client source so your browser can import it:

```bash
# Symlink (Linux/macOS)
ln -s /path/to/zeyos/client/src ./zeyos-client

# Or copy
cp -r /path/to/zeyos/client/src ./zeyos-client
```

Create a minimal `index.html`:

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My ZeyOS App</title>
  <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50 min-h-screen font-sans p-8">

  <div id="app">
    <h1 class="text-2xl font-bold mb-6">My ZeyOS Ticket Dashboard</h1>
    <div id="content">Loading...</div>
  </div>

  <script type="module" src="./app.js"></script>
</body>
</html>
```

---

## Step 2: Initialize the Client

Create `app.js` and set up the ZeyOS client. You have two authentication options:

### Option A: Token Mode

Use this when you already have an OAuth access token. This is most useful for development and controlled demos:

```js
import { createZeyosClient, MemoryTokenStore, normalizeListResult } from './zeyos-client/index.js';

const client = createZeyosClient({
  platform: 'https://cloud.zeyos.com/demo/',
  validate: true,
  auth: {
    mode: 'oauth',
    oauth: {
      tokenStore: new MemoryTokenStore({
        accessToken: 'YOUR_ACCESS_TOKEN',
      }),
    },
  },
});
```

Use session mode or a backend token broker for long-lived browser apps. Do not embed `clientSecret` in shipped browser code just to enable OAuth refresh.

### Option B: Session Mode

Use this when you are already logged into ZeyOS in the same browser:

```js
import { createZeyosClient, normalizeListResult } from './zeyos-client/index.js';

const client = createZeyosClient({
  platform: 'https://cloud.zeyos.com/demo/',
  validate: true,
  auth: {
    mode: 'session',
    session: { enabled: true, credentials: 'include' },
  },
});
```

:::tip
For a real application, never hardcode tokens in source files. Persist them only if you control the environment, or prefer session mode or a backend-assisted token flow. See the [Kanban sample](https://www.zeyos.com/guide/sample-apps/kanban) for a reusable config pattern.
:::

`validate: true` catches wrong field names, enum values and join paths in the browser before a request is sent. To name individual custom fields, add a reviewed `customFields` contract for this instance to the same options:

```js
customFields: {
  tickets: { region: { type: 'string', enum: ['EMEA', 'APAC'] } },
},
```

Custom fields differ per instance, so a contract must ship with a build for that instance. See [custom-field contracts](https://www.zeyos.com/guide/javascript-client/making-requests#custom-field-contracts).

---

## Step 3: Fetch and Display Data

Add a function to load tickets and render them as a table:

```js
async function loadTickets() {
  const result = await client.api.listTickets({
    fields: ['ID', 'ticketnum', 'name', 'status', 'priority', 'duedate', 'assigneduser'],
    filters: { visibility: 0 },
    sort: ['-lastmodified'],
    limit: 50,
  });

  const { data: tickets } = normalizeListResult(result);
  return tickets;
}
```

Key things to note:

- **List operations are POST requests.** The client handles this; you just pass an object.
- **Include `visibility: 0`** to exclude archived/deleted records — on resources that have the column. `transactions`, `payments` and `messages` do not, and filtering on it there returns an opaque HTTP 400.
- **Use `filters` (plural)** for best compatibility across all field types. This handles both simple equality filters and GIN-indexed foreign key fields.
- **Always specify `fields`** to keep payloads small. Without it, every field on every record is returned.
- **Normalise the response.** Use `normalizeListResult()` so list calls share one array/object-wrapper handling path. Use `normalizeCountResult()` for count-only requests.

Now render it:

```js
const STATUS_LABELS = {
  0: 'Not Started', 1: 'Awaiting Acceptance', 2: 'Accepted',
  3: 'Rejected', 4: 'Active', 5: 'Inactive',
  6: 'Feedback Required', 7: 'Testing', 8: 'Cancelled',
  9: 'Completed', 10: 'Failed', 11: 'Booked',
};

const PRIORITY_LABELS = {
  0: 'Lowest', 1: 'Low', 2: 'Medium', 3: 'High', 4: 'Highest',
};

function formatDate(unix) {
  if (!unix) return '';
  return new Date(unix * 1000).toLocaleDateString();
}

function renderTickets(tickets) {
  if (tickets.length === 0) {
    return '<p class="text-gray-500">No tickets found.</p>';
  }

  const rows = tickets.map(t => `
    <tr class="border-t hover:bg-gray-50">
      <td class="py-2 px-3 font-mono text-sm text-gray-400">${t.ticketnum ?? t.ID}</td>
      <td class="py-2 px-3 font-medium">${esc(t.name ?? '')}</td>
      <td class="py-2 px-3 text-sm">${STATUS_LABELS[t.status] ?? t.status}</td>
      <td class="py-2 px-3 text-sm">${PRIORITY_LABELS[t.priority] ?? t.priority}</td>
      <td class="py-2 px-3 text-sm">${formatDate(t.duedate)}</td>
      <td class="py-2 px-3 text-sm">${esc(t.assigneduser ?? '')}</td>
    </tr>
  `).join('');

  return `
    <table class="w-full bg-white rounded-lg shadow text-left">
      <thead>
        <tr class="text-xs uppercase text-gray-500 border-b">
          <th class="py-2 px-3">#</th>
          <th class="py-2 px-3">Name</th>
          <th class="py-2 px-3">Status</th>
          <th class="py-2 px-3">Priority</th>
          <th class="py-2 px-3">Due</th>
          <th class="py-2 px-3">Assigned</th>
        </tr>
      </thead>
      <tbody>${rows}</tbody>
    </table>`;
}

function esc(str) {
  return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;')
    .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
```

Wire it up:

```js
async function boot() {
  try {
    const tickets = await loadTickets();
    document.getElementById('content').innerHTML = renderTickets(tickets);
  } catch (err) {
    document.getElementById('content').innerHTML =
      `<p class="text-red-500">Error: ${esc(err.message)}</p>`;
  }
}

boot();
```

Start your server and open the page -- you should see a table of tickets.

---

## Step 4: Filter by Project

Add a project selector above the table. First, load the available projects:

```js
async function loadProjects() {
  try {
    const result = await client.api.listProjects({
      fields: ['ID', 'name'],
      filters: { visibility: 0 },
      sort: ['+name'],
      limit: 500,
    });
    return normalizeListResult(result).data;
  } catch {
    return [];
  }
}
```

Then build a `<select>` and reload tickets when the selection changes:

```js
function renderProjectFilter(projects, onSelect) {
  const options = projects.map(p =>
    `<option value="${p.ID}">${esc(p.name)}</option>`
  ).join('');

  return `
    <select id="project-filter"
      class="border rounded-lg px-3 py-2 text-sm mb-4">
      <option value="">All Projects</option>
      ${options}
    </select>`;
}

async function boot() {
  const projects = await loadProjects();
  const app = document.getElementById('content');

  app.innerHTML = renderProjectFilter(projects) + '<div id="tickets">Loading...</div>';

  const loadAndRender = async (projectId) => {
    const filters = { visibility: 0 };
    if (projectId) filters.project = Number(projectId);

    const result = await client.api.listTickets({
      fields: ['ID', 'ticketnum', 'name', 'status', 'priority', 'duedate', 'assigneduser'],
      filters,
      sort: ['-lastmodified'],
      limit: 50,
    });
    const { data: tickets } = normalizeListResult(result);
    document.getElementById('tickets').innerHTML = renderTickets(tickets);
  };

  document.getElementById('project-filter').addEventListener('change', e => {
    loadAndRender(e.target.value);
  });

  await loadAndRender('');
}
```

---

## Step 5: Create a New Ticket

Add a simple form and use the `createTicket` API. Note: **create operations use PUT**, not POST.

```js
function renderCreateForm() {
  return `
    <form id="create-form" class="bg-white rounded-lg shadow p-4 mb-6 flex gap-3 items-end">
      <div class="flex-1">
        <label class="block text-xs font-medium text-gray-600 mb-1">Ticket Name</label>
        <input id="f-name" type="text" placeholder="What needs to be done?"
          class="w-full border rounded-lg px-3 py-2 text-sm" required>
      </div>
      <div>
        <label class="block text-xs font-medium text-gray-600 mb-1">Priority</label>
        <select id="f-priority" class="border rounded-lg px-3 py-2 text-sm">
          <option value="0">Lowest</option>
          <option value="1">Low</option>
          <option value="2" selected>Medium</option>
          <option value="3">High</option>
          <option value="4">Highest</option>
        </select>
      </div>
      <button type="submit"
        class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700">
        Create
      </button>
    </form>`;
}
```

Handle form submission:

```js
document.getElementById('create-form').addEventListener('submit', async e => {
  e.preventDefault();
  const name = document.getElementById('f-name').value.trim();
  if (!name) return;

  try {
    await client.api.createTicket({
      name,
      priority: Number(document.getElementById('f-priority').value),
      status: 0,
      visibility: 0,
    });

    document.getElementById('f-name').value = '';
    await loadAndRender(''); // Refresh the list
  } catch (err) {
    alert(`Create failed: ${err.message}`);
  }
});
```

For generated create and update operations, the flat input style works for normal record fields.

If a create fails with a timeout or connection error, it may still have succeeded. Refresh the list and let the user check before they submit again. The client does not retry these failures for writes, so it cannot create a duplicate on its own, but a second click can. See [uncertain write outcomes](https://www.zeyos.com/guide/javascript-client/making-requests#uncertain-write-outcomes).

---

## Step 6: Update a Ticket

Update a record by passing the record ID and the changed fields:

```js
async function updateTicketStatus(ticketId, newStatus) {
  const updated = await client.api.updateTicket({
    ID: ticketId,
    status: newStatus,
  });

  // The response contains the full updated record -- use it to
  // confirm the change was applied
  return updated;
}
```

If you prefer explicit separation, `body` and `data` are also supported:

```js
await client.api.updateTicket({ ID: ticketId, body: { status: newStatus } });
```

---

## Step 7: Delete a Ticket

Deletion is straightforward:

```js
async function removeTicket(ticketId) {
  await client.api.deleteTicket({ ID: ticketId });
}
```

Always confirm with the user before deleting -- there is no undo.

---

## Step 8: Fetch Related Data

Most ZeyOS entities are related to each other. For example, tickets can have tasks. To load tasks for a specific ticket:

```js
async function loadTasks(ticketId) {
  const result = await client.api.listTasks({
    fields: ['ID', 'tasknum', 'name', 'status', 'duedate', 'assigneduser'],
    filters: { ticket: ticketId, visibility: 0 },
    sort: ['+name'],
    limit: 200,
  });
  return normalizeListResult(result).data;
}
```

You can also use **dot-notation joins** to pull fields from related records in a single request:

```js
const tickets = await client.api.listTickets({
  fields: {
    Id: 'ID',
    Name: 'name',
    ProjectName: 'project.name',
    AssignedTo: 'assigneduser.name',
    AccountName: 'account.lastname',
  },
  filters: { visibility: 0 },
  limit: 50,
});
```

This returns flattened objects like `{ Id: 42, Name: '...', ProjectName: 'Acme', AssignedTo: 'Jane', AccountName: 'Lightexx AG' }` -- no extra API calls needed. Each step of a path must be a documented relationship on that resource; `client.schema.describe('tickets')` lists them, and `validate: true` rejects the rest.

---

## Step 9: Handle Errors Gracefully

Every API error throws a `ZeyosApiError` with rich context:

```js
import { ZeyosApiError } from './zeyos-client/index.js';

try {
  await client.api.getTicket({ ID: 999999 });
} catch (err) {
  if (err instanceof ZeyosApiError) {
    switch (err.status) {
      case 401:
        // Token expired and auto-refresh failed
        showLoginScreen();
        break;
      case 403:
        showMessage('You do not have permission to view this record.');
        break;
      case 404:
        showMessage('Record not found.');
        break;
      default:
        showMessage(`Request failed (HTTP ${err.status}). Please try again.`);
    }
  } else {
    // Network error, timeout, etc.
    showMessage('Network error. Please check your connection.');
  }
}
```

---

## Step 10: Working with Dates

All ZeyOS date fields are **Unix timestamps in seconds** (not milliseconds). This catches most JavaScript developers off guard since `Date.now()` returns milliseconds.

```js
// Reading: multiply by 1000
const jsDate = new Date(ticket.duedate * 1000);

// Writing: divide by 1000
const duedate = Math.floor(new Date('2026-06-15').getTime() / 1000);
await client.api.updateTicket({ ID: id, body: { duedate } });

// Checking overdue
const isOverdue = ticket.duedate * 1000 < Date.now();
```

---

## Summary: Quick Reference Card

| Task | Code |
|------|------|
| List records | `client.api.listTickets({ fields: [...], filters: {...}, sort: [...], limit: N })` |
| Get one record | `client.api.getTicket({ ID: 42 })` |
| Create | `client.api.createTicket({ name: '...', status: 0, visibility: 0 })` |
| Update | `client.api.updateTicket({ ID: 42, body: { status: 4 } })` |
| Delete | `client.api.deleteTicket({ ID: 42 })` |
| Related data | `client.api.listTasks({ filters: { ticket: 42 } })` |
| Dot-notation join | `fields: { Account: 'account.lastname', Agent: 'assigneduser.name' }` |
| Date read | `new Date(record.duedate * 1000)` |
| Date write | `Math.floor(date.getTime() / 1000)` |

---

## Where to Go Next

- **[Practical Guide](https://www.zeyos.com/guide/javascript-client/practical-guide)** -- deeper coverage of `filter` vs `filters`, token persistence, and optimistic UI patterns
- **[Making Requests](https://www.zeyos.com/guide/javascript-client/making-requests)** -- full reference for field selection, sorting, pagination, extended data, and error handling
- **[Data Retrieval](https://www.zeyos.com/guide/api-reference/data-retrieval)** -- the complete REST query language with advanced filter operators, full-text search, and composite expressions
- **[Kanban Sample](https://www.zeyos.com/guide/sample-apps/kanban)** -- a reusable sample browser app with drag-and-drop, modals, and session detection
- **[Authentication](https://www.zeyos.com/guide/javascript-client/authentication)** -- OAuth 2.0 flows, session mode, legacy auth, and token management

---

## Available Resources

The ZeyOS API provides access to over 50 resource types. Here are the ones most commonly used when building custom frontends:

| Resource | Operations | Use case |
|----------|-----------|----------|
| `Tickets` | list, get, create, update, delete | Issue tracking, support, project work |
| `Tasks` | list, get, create, update, delete | Task management within tickets/projects |
| `Accounts` | list, get, create, update, delete | CRM contacts and companies |
| `Projects` | list, get, create, update, delete | Project organisation |
| `Contacts` | list, get, create, update, delete | Contact details (addresses, phone, email) |
| `Appointments` | list, get, create, update, delete | Calendar and scheduling |
| `Transactions` | list, get, create, update, delete | Invoices, quotes, orders |
| `Items` | list, get, create, update, delete | Products and services |
| `Documents` | list, get, create, update, delete | File management |
| `Notes` | list, get, create, update, delete | Notes attached to any entity |
| `Messages` | list, get, create, update, delete | Email and messaging |
| `Opportunities` | list, get, create, update, delete | Sales pipeline |
| `Users` | list, get | Team members and permissions |

Every resource follows the same API patterns shown in this guide. Once you know how to work with tickets, you can work with any resource.

---

Canonical: https://www.zeyos.com/guide/tutorials/integration-architecture

# Integration Architecture

Choose the integration model before you choose the framework. In ZeyOS projects, the main design decision is usually where authentication happens and where long-lived business operations run.

## Comparison

| Model | Where code runs | Auth model | Best for | Main constraint |
|-------|-----------------|------------|----------|-----------------|
| Browser session mode | Browser | Existing `ZEYOSID` session cookie | Internal tools on the same origin, embedded UIs, operator-facing apps | Requires browser session access and compatible credentialed requests |
| Browser token mode | Browser | Pre-obtained OAuth tokens | Development, controlled demos, pre-provisioned browser apps | Do not embed a client secret in production browser code |
| Server token mode | Server, worker, cron, API backend | OAuth tokens stored server-side | Integrations, sync jobs, scheduled tasks, multi-step business logic | Requires token persistence and refresh handling |

## Recommended Choices

### Use browser session mode when:

- the user is already logged into ZeyOS
- the UI runs on the same origin or an allowed credentialed origin
- you want the simplest browser auth flow with no token storage in the page

### Use browser token mode when:

- you already have tokens from a trusted flow
- you are building a local demo or internal prototype
- you can avoid embedding client secrets in shipped browser code

### Use server token mode when:

- the application runs without a live browser session
- you need retries, scheduling, background work, or webhook handling
- you want one place to centralize token refresh and request logging

Browser token mode is intentionally limited. Use it for pre-obtained access tokens and controlled demos. If you need authorization-code exchange or automatic refresh, move that responsibility to server token mode because the current client helpers require `clientId` and `clientSecret`.

## Interface Selection

| Need | Interface |
|------|-----------|
| JavaScript application code with full API coverage | `@zeyos/client` |
| Shell-driven operational workflows | `zeyos` CLI |
| Another language or custom SDK | REST/OpenAPI |

## Cross-Cutting Rules

- List operations are `POST` requests in ZeyOS, even though they behave like queries.
- Prefer `filters` in client code for consistent handling of scalar and foreign-key fields.
- Use `visibility: 0` on resources that **have** the column — `accounts`, `contacts`, `tickets`, `tasks`, `projects`, `items`, `documents`, `notes`, `opportunities`, `appointments`, `campaigns`, `mailinglists`, `storages`, `contracts`, `ledgers` — unless you want archived (`1`) or deleted (`2`) records. **Most other resources have no `visibility` column** — `transactions` (and every billing/procurement entity), `payments`, `messages`, `actionsteps`, `addresses`, `users`, `prices`, `dunning` — and filtering on it there returns an opaque HTTP 400. Check with `zeyos describe <entity>` when unsure.
- Use `body: { ... }` for updates that also pass `ID`.
- Treat count-enabled responses defensively. Different endpoints or client layers may return either a count wrapper or a list wrapper with count metadata.

## Recommended Reading Order

1. [Browser UI Playbook](https://www.zeyos.com/guide/tutorials/build-your-own-zeyos-frontend) for user-facing frontends
2. [Server-Side Integrations](https://www.zeyos.com/guide/tutorials/server-side-integrations) for services, workers, and scheduled jobs
3. [Making Requests](https://www.zeyos.com/guide/javascript-client/making-requests) for the full request model

---

Canonical: https://www.zeyos.com/guide/tutorials/server-side-integrations

# Server-Side Integrations

This guide covers the recommended backend pattern for ZeyOS: keep OAuth tokens on the server, refresh them automatically, and isolate business sync logic behind small job functions.

## When to Use This Pattern

Use a server-side integration when you are building:

- scheduled sync jobs
- background workers
- webhook handlers
- internal APIs that proxy or enrich ZeyOS data
- automation services that should not depend on a live browser session

## 1. Store State Safely

A sync job keeps two pieces of state on disk: OAuth tokens and its checkpoint. Both need the same guarantees, so write them through one helper. The helper tells a missing file apart from malformed data and from a permission failure, and it replaces files atomically with private permissions. A crash during a write leaves the previous version intact.

```js
// state-file.mjs
import { randomBytes } from 'node:crypto';
import { open, readFile, rename, rm } from 'node:fs/promises';
import { basename, dirname, join } from 'node:path';

export class StateFileError extends Error {
  constructor(kind, path, action) {
    super(`${kind} state file ${basename(path)}: ${action}`);
    this.name = 'StateFileError';
    this.kind = kind; // 'permission' | 'malformed' | 'unreadable'
  }
}

/** Resolves to undefined only when the file does not exist. */
export async function readStateFile(path) {
  let text;
  try {
    text = await readFile(path, 'utf8');
  } catch (err) {
    if (err.code === 'ENOENT') return undefined;
    if (err.code === 'EACCES' || err.code === 'EPERM') {
      throw new StateFileError('permission', path, 'grant the job user read access.');
    }
    throw new StateFileError('unreadable', path, `check the file (${err.code}).`);
  }
  try {
    return JSON.parse(text);
  } catch {
    // JSON parser messages can quote the input, and this file holds secrets.
    throw new StateFileError('malformed', path, 'restore it from a backup or re-authenticate.');
  }
}

/** Temp file in the same directory, flushed, then renamed over the target. */
export async function writeStateFile(path, value) {
  const suffix = `${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
  const tempPath = join(dirname(path), `.${basename(path)}.${suffix}`);
  try {
    const handle = await open(tempPath, 'wx', 0o600);
    try {
      await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`);
      await handle.sync();
    } finally {
      await handle.close();
    }
    await rename(tempPath, path);
  } catch (err) {
    await rm(tempPath, { force: true });
    if (err.code === 'EACCES' || err.code === 'EPERM') {
      throw new StateFileError('permission', path, 'grant the job user write access to the directory.');
    }
    throw err;
  }
}
```

The client requires only a `get()` and `set()` pair from a token store. This one is for development. In production, back the same two methods with a secrets manager or database.

```js
// token-store.mjs
import { rm } from 'node:fs/promises';
import { normalizeTokenSet } from '@zeyos/client';
import { readStateFile, StateFileError, writeStateFile } from './state-file.mjs';

export class JsonFileTokenStore {
  constructor(path) {
    this.path = path;
  }

  async get() {
    const stored = await readStateFile(this.path);
    if (stored === undefined) return null; // Not signed in yet.

    const tokenSet = normalizeTokenSet(stored);
    if (!tokenSet) {
      throw new StateFileError('malformed', this.path, 'it holds no token set; re-authenticate.');
    }
    return tokenSet;
  }

  async set(tokenSet) {
    if (!tokenSet) {
      await rm(this.path, { force: true }); // client.auth.clearTokenSet()
      return;
    }
    await writeStateFile(this.path, tokenSet);
  }
}
```

Only a missing file means "not signed in". A corrupt or unreadable store throws, and the error reaches the caller of the API method. Treating those cases as empty would discard a working refresh token and send the job to a confusing `401`.

Keep the file outside your repository and do not log its contents.

### Sharing a Token Store Between Workers

The client coordinates refreshes only **inside one client instance**: concurrent requests share a single refresh. Nothing coordinates separate processes. The client has no lock, no compare-and-swap and no hook around a refresh, and a token store sees only the resulting `get()` and `set()` calls. A lock therefore cannot be added inside a store.

The client does read the store at the start of every operation, so a worker picks up tokens written by another process on its next call. What remains is the race between two workers refreshing at the same moment. Choose one approach:

| Approach | How it works | Use when |
|----------|--------------|----------|
| **Single refresher** (recommended) | Workers set `autoRefresh: false` and only read the store. One process calls `client.oauth2.refreshToken()` before expiry. | Any multi-process deployment |
| **Lock around an explicit refresh** | Each worker sets `autoRefresh: false` and takes a lock before calling `client.oauth2.refreshToken()`, as shown below. | There is no natural owner for a refresher |
| **Accept duplicate refreshes** | Leave `autoRefresh` on and let workers race. | Only after confirming your instance keeps the previous refresh token valid |

The third approach depends on server behavior this client does not establish. Its code handles a response with no new refresh token by keeping the old one, and also handles a rotated one, but ZeyOS's rotation policy is not documented here. If refresh tokens rotate, the losing worker presents a used token, its refresh fails, and the failed call surfaces as an error even though the store already holds valid tokens. Test this against a sandbox instance before relying on it.

```js
// refresh.mjs: run this from workers configured with autoRefresh: false.
import { withLockFile } from './lock-file.mjs';

const REFRESH_MARGIN_SECONDS = 120;

export async function refreshIfDue(client, lockPath) {
  await withLockFile(lockPath, async () => {
    // Re-read inside the lock: another worker may have finished a refresh.
    const tokenSet = await client.auth.getTokenSet();
    const now = Math.floor(Date.now() / 1000);
    if (tokenSet?.expiresAt == null || tokenSet.expiresAt > now + REFRESH_MARGIN_SECONDS) return;
    await client.oauth2.refreshToken();
  });
}
```

A failed `set()` needs attention in every approach. The server has already issued new tokens at that point, and if it rotated the refresh token, the job has lost the only copy. A failed write during a refresh before the request is also swallowed by the client, which continues with the expired token, so monitor store write failures separately.

The lock uses an exclusive create, which either succeeds or reports who holds it:

```js
// lock-file.mjs
import { open, readFile, rm, stat } from 'node:fs/promises';

export class LockHeldError extends Error {
  constructor({ pid, ageSeconds, stale }) {
    super(`${stale ? 'Stale lock' : 'Another run holds the lock'}: pid ${pid ?? 'unknown'}, ${ageSeconds}s old.`);
    this.name = 'LockHeldError';
    this.stale = stale; // true: no live run owns it, so nothing will clear it.
  }
}

function isRunning(pid) {
  try {
    process.kill(pid, 0); // Signal 0 tests for the process without touching it.
    return true;
  } catch (err) {
    return err.code === 'EPERM'; // It exists under another user.
  }
}

async function describeLock(path, maxAgeMs) {
  try {
    const [text, { mtimeMs }] = await Promise.all([readFile(path, 'utf8'), stat(path)]);
    const pid = Number.parseInt(text, 10);
    const ageMs = Date.now() - mtimeMs;
    const owned = pid > 0;
    return {
      pid: owned ? pid : null,
      ageSeconds: Math.round(ageMs / 1000),
      // An empty file is a holder between create and write, until it ages out.
      stale: (owned && !isRunning(pid)) || ageMs > maxAgeMs,
    };
  } catch (err) {
    if (err.code === 'ENOENT') return { pid: null, ageSeconds: 0, stale: false }; // Just released.
    throw err;
  }
}

export async function withLockFile(path, task, { maxAgeMs = 60 * 60_000 } = {}) {
  let handle;
  try {
    handle = await open(path, 'wx', 0o600);
  } catch (err) {
    if (err.code === 'EEXIST') throw new LockHeldError(await describeLock(path, maxAgeMs));
    throw err;
  }
  try {
    await handle.writeFile(`${process.pid}\n`);
    return await task();
  } finally {
    await handle.close();
    await rm(path, { force: true });
  }
}
```

A process that is killed leaves its lock behind. The next run reports the lock as stale, because its process is gone or it is older than `maxAgeMs`, and exits non-zero so a monitor sees that the job has stopped. It does not remove the lock itself: confirm that the recorded process is gone, then delete the file. Set `maxAgeMs` above your longest run, because a process ID can be reused after a reboot. This lock works on one host's local filesystem. Across hosts, use a lock from your database or scheduler.

## 2. Build the Client

The code in Sections 2, 3 and 4 forms one module, `sync.mjs`.

```js
import { createZeyosClient } from '@zeyos/client';
import { JsonFileTokenStore } from './token-store.mjs';

const tokenStore = new JsonFileTokenStore('/var/lib/my-sync/tokens.json');

const client = createZeyosClient({
  platform: 'https://cloud.zeyos.com/demo/',
  validate: true,
  timeoutMs: 30_000,
  auth: {
    mode: 'oauth',
    oauth: {
      clientId: process.env.ZEYOS_CLIENT_ID,
      clientSecret: process.env.ZEYOS_CLIENT_SECRET,
      tokenStore,
      autoRefresh: true,
    },
  },
});
```

- `validate: true` rejects unknown fields, invalid enum values and invalid join paths before any request is sent. Validation is off by default in JavaScript.
- `timeoutMs` bounds each attempt. Without it, a stalled connection can wait forever and the retry logic never runs.
- `autoRefresh: true` refreshes an expired access token before a request, or after a bearer `401`. Your token store must persist the result.
- If the job names individual `extdata` keys, add a reviewed [`customFields` contract](https://www.zeyos.com/guide/javascript-client/making-requests#custom-field-contracts) bound to this instance.

## 3. Fetch One Page

ZeyOS has **no server-side cursor**. The API offers `limit` (up to 10000), `offset`, `sort`, `filters` and `count`, and a sync job builds its position from those.

Do not page a changing dataset with `offset`, and that includes `client.paginate()`, which is offset-based. When a record on an earlier page changes, it moves to the end of a `lastmodified` ordering, every later row shifts up by one, and the next page silently skips a record.

Instead, use **keyset pagination**: sort by `lastmodified` with `ID` as the tie-breaker, and ask for rows strictly after the last one you processed. A row that changes during the run moves further ahead of the position, so it is read later and never skipped.

```js
import { normalizeListResult } from '@zeyos/client';

export const PAGE_SIZE = 500;
const TICKET_FIELDS = ['ID', 'ticketnum', 'name', 'status', 'priority', 'visibility', 'lastmodified'];

/** Rows strictly after (lastmodified, ID) in the sort order below. */
function ticketPageQuery(after) {
  return {
    fields: TICKET_FIELDS,
    // No visibility filter: archiving a ticket must reach the downstream system.
    filters: {
      1: ['OR',
        { lastmodified: { '>': after.lastmodified } },
        { 2: ['AND',
          { lastmodified: { '=': after.lastmodified } },
          { ID: { '>': after.ID } },
        ] },
      ],
    },
    sort: ['+lastmodified', '+ID'],
    limit: PAGE_SIZE,
  };
}

export async function fetchTicketPage(after, { signal } = {}) {
  const result = await client.api.listTickets(ticketPageQuery(after), { signal });
  return normalizeListResult(result).data;
}
```

The `ID` tie-breaker matters because `lastmodified` has one-second resolution. A bulk update can give more rows the same timestamp than fit on one page, and a `lastmodified`-only position would stall or lose rows at that boundary.

Keep each sync unit focused: one resource, one query shape, one return shape. If you use aliases in `fields`, keep `lastmodified` and `ID` under their real names, because the position is read from them.

## 4. Run a Resumable Sync

A pass reads every row after its starting point, and may span several runs. The checkpoint stores two positions and is saved only after the downstream system has accepted the rows:

- `through` is where the last complete pass ended. The next pass starts one overlap window behind it.
- `position` is how far the current pass has got, or `null` between passes. A run that stops early leaves it set, and the next run continues from there.

The overlap applies only when a new pass starts. If every run began behind `through`, a window holding more rows than one run can process would use up the budget each time, and the job would never get past those rows. Because a resumed pass continues from `position`, which only moves forward, every run that processes a row makes lasting progress.

```js
import { readStateFile, writeStateFile } from './state-file.mjs';

const SYNC_PLATFORM = 'https://cloud.zeyos.com/demo';
const OVERLAP_SECONDS = 300;
const START = { lastmodified: 0, ID: 0 };

const isAfter = (a, b) =>
  a.lastmodified > b.lastmodified || (a.lastmodified === b.lastmodified && a.ID > b.ID);

function positionOf(row) {
  if (!Number.isSafeInteger(row?.lastmodified) || !Number.isSafeInteger(row?.ID)) {
    throw new Error(`Ticket ${row?.ID} has no usable sync position.`);
  }
  return { lastmodified: row.lastmodified, ID: row.ID };
}

async function loadCheckpoint(path) {
  const saved = await readStateFile(path);
  if (saved === undefined) return { through: START, position: null };
  if (saved?.platform !== SYNC_PLATFORM || saved?.resource !== 'tickets') {
    throw new Error('Checkpoint belongs to another instance or resource.');
  }
  return {
    through: positionOf(saved.through),
    position: saved.position === null ? null : positionOf(saved.position),
  };
}

function saveCheckpoint(path, checkpoint) {
  return writeStateFile(path, {
    platform: SYNC_PLATFORM,
    resource: 'tickets',
    ...checkpoint,
    savedAt: new Date().toISOString(), // For operators; the sync never reads it.
  });
}

export async function runTicketSync({ checkpointPath, upsert, signal, maxPages = 10_000 }) {
  const { through, position: resumed } = await loadCheckpoint(checkpointPath);

  // Only a new pass re-reads the overlap window, so a crash or late commit is covered
  // without letting that window hold the job back.
  let position = resumed ?? { lastmodified: Math.max(0, through.lastmodified - OVERLAP_SECONDS), ID: 0 };

  // Offline preflight: fail on a bad query before the first request.
  const preflight = client.schema.validate('listTickets', ticketPageQuery(position));
  if (!preflight.valid) throw new Error(`Invalid sync query: ${preflight.errors[0].code}`);

  let records = 0;
  try {
    for (let page = 0; page < maxPages; page += 1) {
      const rows = await fetchTicketPage(position, { signal });

      for (const row of rows) {
        signal?.throwIfAborted();
        const next = positionOf(row);
        // Stop rather than advance past rows if the server ignored the ordering.
        if (!isAfter(next, position)) throw new Error(`Ticket ${row.ID} arrived out of order.`);
        await upsert(row, { signal });
        position = next;
        records += 1;
      }

      if (rows.length < PAGE_SIZE) {
        // The overlap can end behind `through`, so never move it backwards.
        const reached = isAfter(position, through) ? position : through;
        await saveCheckpoint(checkpointPath, { through: reached, position: null });
        return { complete: true, records, checkpoint: reached };
      }
      await saveCheckpoint(checkpointPath, { through, position });
    }
  } catch (err) {
    // Keep the rows already accepted, so a slow page cannot repeat forever. The
    // original failure matters more than a failed save here.
    await saveCheckpoint(checkpointPath, { through, position }).catch(() => {});
    throw err;
  }
  return { complete: false, records, checkpoint: position }; // Budget reached: resume next run.
}
```

Run it behind a lock, so scheduled runs cannot overlap, and with a budget for the whole job:

```js
import { describeFailure } from './failures.mjs';
import { LockHeldError, withLockFile } from './lock-file.mjs';

export async function main(upsert) {
  try {
    const summary = await withLockFile('/var/lib/my-sync/tickets.lock', () => runTicketSync({
      checkpointPath: '/var/lib/my-sync/tickets.checkpoint.json',
      upsert,
      signal: AbortSignal.timeout(15 * 60_000), // Budget for all pages and retries.
    }));
    console.log(JSON.stringify({ job: 'ticket-sync', ...summary }));
  } catch (err) {
    // An overlapping run is normal. A stale lock falls through and fails the job,
    // because every later run would otherwise stop here without anyone noticing.
    if (err instanceof LockHeldError && !err.stale) {
      console.log(JSON.stringify({ job: 'ticket-sync', skipped: err.message }));
      return;
    }
    console.error(JSON.stringify(describeFailure(err, { resource: 'tickets', operationId: 'listTickets' })));
    process.exitCode = 1;
  }
}
```

### Crash and Change Behavior

| Event | Result |
|-------|--------|
| Hard crash during a page | The checkpoint still holds the end of the previous page. The next run repeats this page, so rows may be delivered twice. |
| Timeout or error during a page | The accepted rows are saved first, and the next run continues after them. A row may be delivered twice if `upsert` committed but did not return. |
| Crash after a complete pass | The next run starts a new pass, which re-reads the overlap window. |
| Record changes during the run | Its `lastmodified` moves ahead of the position, so it is read later in this run or in the next one. |
| Record changes repeatedly while the job runs | It may be delivered several times. The downstream write keeps the newest version. |
| Write commits late with an older timestamp | Covered if it lands within the overlap window behind the point where the pass ended. |
| Budget or `maxPages` reached | The run stops with saved progress. A `maxPages` stop returns `complete: false` and a budget stop throws. The next run continues from `position`, without repeating the overlap. |
| More rows share a timestamp, or sit in the overlap, than one run can process | Several runs finish the pass. Each continues inside the same timestamp, because the position includes `ID`. |
| Rows change faster than the job processes them | The position still advances, but the pass may not finish until the rate drops. |
| A killed run leaves its lock | The next run exits non-zero with a stale-lock failure and does not sync until an operator clears it. |

### Make Downstream Writes Idempotent

Every row can arrive more than once, by design. `upsert` must therefore key on the instance and `ID`, and must refuse to replace newer data. It receives the job's `signal` as `upsert(row, { signal })`: pass that to your database or HTTP call, so a slow downstream write cannot outlive the job's budget.

```sql
INSERT INTO zeyos_tickets (instance, id, name, status, visibility, lastmodified)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (instance, id) DO UPDATE
SET name = excluded.name,
    status = excluded.status,
    visibility = excluded.visibility,
    lastmodified = excluded.lastmodified
WHERE excluded.lastmodified >= zeyos_tickets.lastmodified;
```

Treat `visibility` as data: `1` is archived and `2` is deleted. Resources without that column need a different rule, and `client.schema.describe('<resource>')` shows what each one has.

### Reconcile Deletions

An incremental sync sees only rows that still exist and match. A hard-deleted record simply stops appearing, and whether archiving updates `lastmodified` is not guaranteed here. Rows with an empty `lastmodified` never match the filter either. Run a periodic ID pass, for example nightly, to catch all three:

```js
export async function listAllTicketIds({ signal } = {}) {
  const ids = [];
  let after = 0;
  for (;;) {
    const result = await client.api.listTickets({
      fields: ['ID'],
      filters: { ID: { '>': after } },
      sort: ['+ID'],
      limit: PAGE_SIZE,
    }, { signal });
    const { data: rows } = normalizeListResult(result);
    ids.push(...rows.map((row) => row.ID));
    if (rows.length < PAGE_SIZE) return ids;
    after = rows.at(-1).ID;
  }
}
```

Compare this list with your downstream IDs. IDs missing downstream need a `getTicket` and an upsert. Before removing a record that is missing upstream, require a pass that finished without errors, because an incomplete list looks exactly like a mass deletion. Keep that removal explicit and reviewable.

### What "Complete" Means

`complete: true` means one thing: a pass ended on a short page, so at that moment no row after the position was visible to this credential. `complete: false` means the pass continues on the next run. It does **not** mean the downstream system equals ZeyOS.

- **It is point-in-time.** Records can change immediately afterwards, and the next run picks them up.
- **It is scoped to the credential.** Rows the authenticated user cannot read are not returned, and nothing reports them as absent.
- **`count` is not a completeness check.** It returns only a number, ignores `sort`, `offset` and `limit`, and changes while the job runs.
- **Timestamps are Unix seconds in UTC.** They carry no timezone, so no local conversion belongs in the position. Only compare `lastmodified` with other server values, never with your own clock.
- **The overlap is a tuning value.** Five minutes is an example, not a guarantee about commit delays on your instance. It is measured from where a pass ended, so a pass that chases live changes for longer than the overlap narrows that cover.
- **Joined data is not tracked.** A change to a linked contact does not necessarily update the ticket's `lastmodified`. The [composite filter example](https://www.zeyos.com/guide/api-reference/data-retrieval#composite-filters-and-or-not) shows how to include a joined timestamp, but the two-field position in this tutorial does not cover that case.
- **Check this query on a sandbox instance.** The composite filter and two-field sort follow the API reference, and `positionOf` and the ordering check stop the job if a server behaves differently. Bundled metadata does not list `lastmodified` as indexed, so test page times on large tables before choosing `PAGE_SIZE`.

## 5. Use `client.request()` for Advanced Cases

Use the generated methods first. Drop to `client.request()` when you need a custom path or an operation outside your current helper layer.

By operation ID, which keeps the validation and read classification of the generated method:

```js
const response = await client.request({
  service: 'api',
  operationId: 'listTickets',
  body: {
    filters: { visibility: 0, priority: 4 },
    limit: 25,
  },
});
```

By explicit path and method:

```js
const response = await client.request({
  service: 'api',
  method: 'POST',
  path: '/tickets/',
  body: {
    filters: { visibility: 0, status: 4 },
    limit: 25,
  },
});
```

A method and path call has no operation schema, which costs you two protections:

- **No preflight validation**, even with `validate: true`. Unknown fields, invalid enum values, join paths and custom-field keys all reach the server unchecked.
- **No read classification.** A `POST` list sent this way counts as a possible write, so it is not retried after a network error, a timeout or a `503` unless you pass `retryOnNetworkError: true`.

## 6. Report Failures Without Leaking Data

Log what an operator can act on: the resource, the operation, a category and the next step. `err.body` and `err.message` can contain record data from the response, so neither belongs in a log line.

```js
// failures.mjs
import { ZeyosApiError, ZeyosValidationError } from '@zeyos/client';
import { LockHeldError } from './lock-file.mjs';
import { StateFileError } from './state-file.mjs';

const NEXT_STEP = {
  validation: 'Fix the query; no request was sent.',
  authentication: 'Re-authenticate and check the token store.',
  permission: 'Grant the integration user access to this resource.',
  not_found: 'Reconcile the record ID.',
  rate_limited: 'Reduce request volume; the next run resumes from the checkpoint.',
  upstream: 'The next run resumes from the checkpoint. Reconcile any write first.',
  request: 'Review the request against the API reference.',
  timeout: 'Check connectivity. Reconcile any write before retrying it.',
  cancelled: 'The job budget ended or a caller aborted. The next run resumes.',
  state_file: 'Repair the state file named in the message.',
  stale_lock: 'Confirm the recorded process is gone, then delete the lock file.',
  network: 'Check connectivity. Reconcile any write before retrying it.',
};

function categorize(err) {
  if (err instanceof ZeyosValidationError) return 'validation';
  if (err instanceof StateFileError) return 'state_file';
  if (err instanceof LockHeldError) return 'stale_lock';
  if (err instanceof ZeyosApiError) {
    if (err.status === 401) return 'authentication';
    if (err.status === 403) return 'permission';
    if (err.status === 404) return 'not_found';
    if (err.status === 429) return 'rate_limited';
    return err.status >= 500 ? 'upstream' : 'request';
  }
  if (err.isTimeout) return 'timeout';
  if (err.name === 'AbortError' || err.name === 'TimeoutError') return 'cancelled';
  return 'network';
}

export function describeFailure(err, context) {
  const category = categorize(err);
  return {
    ...context,
    category,
    ...(err.status ? { status: err.status } : {}),
    ...(err instanceof ZeyosValidationError
      ? { problems: err.errors.map(({ field, code }) => ({ field, code })) }
      : {}),
    ...(err instanceof StateFileError || err instanceof LockHeldError ? { detail: err.message } : {}),
    next: NEXT_STEP[category],
  };
}
```

```js
import { describeFailure } from './failures.mjs';

try {
  await client.api.updateTicket({ ID: 42, body: { status: 7 } });
} catch (err) {
  console.error(JSON.stringify(describeFailure(err, { resource: 'tickets', operationId: 'updateTicket', ID: 42 })));
  throw err;
}
```

Reads are safe to run again, which is why the sync can simply stop and resume. Writes are not: the `updateTicket` call above may have succeeded even if it threw. See [Retries and Rate Limiting](https://www.zeyos.com/guide/javascript-client/making-requests#retries-and-rate-limiting) for what the client retries, and [uncertain write outcomes](https://www.zeyos.com/guide/javascript-client/making-requests#uncertain-write-outcomes) before you retry one.

## 7. Know the Limits

- Prefer `filters` in client code.
- Use `visibility: 0` on resources that **have** the column — `accounts`, `contacts`, `tickets`, `tasks`, `projects`, `items`, `documents`, `notes`, `opportunities`, `appointments`, `campaigns`, `mailinglists`, `storages`, `contracts`, `ledgers` — unless you want archived (`1`) or deleted (`2`) records, as a sync usually does. **Most other resources have no `visibility` column** — `transactions` (and every billing/procurement entity), `payments`, `messages`, `actionsteps`, `addresses`, `users`, `prices`, `dunning` — and filtering on it there returns an opaque HTTP 400. Check with `zeyos describe <entity>` when unsure.
- Use explicit `body` objects for update operations that pass `ID`.
- Treat count-enabled responses defensively and normalize them in one place inside your backend.

## Next Steps

- [Integration Architecture](https://www.zeyos.com/guide/tutorials/integration-architecture)
- [Making Requests](https://www.zeyos.com/guide/javascript-client/making-requests)
- [Authentication](https://www.zeyos.com/guide/javascript-client/authentication)

---

Canonical: https://www.zeyos.com/ixml

# Introduction

iXML is an XML-based server-side language used by ZeyOS for services, workflows, data orchestration, and integration logic. It is the primary orchestration layer for ZeyOS applications, responsible for handling HTTP request/response cycles, database interactions, file processing, external API communication, and business rule enforcement. If you are new to ZeyOS customization, iXML is the first language to learn.

Unlike general-purpose programming languages, iXML is purpose-built for the ZeyOS runtime environment. Every iXML document is valid XML, which means it inherits XML's strict syntactic rules — proper nesting, mandatory closing tags, attribute quoting, and entity escaping. This design decision allows ZeyOS to parse iXML documents with standard XML parsers and provides IDE support through DTD and XSD schemas. The tradeoff is that certain characters common in programming (such as `<` and `>`) must be entity-escaped when they appear inside attribute values.

iXML follows a **substitution-based evaluation model** rather than the expression-based evaluation found in most mainstream languages. Variables are referenced through dollar-sign substitution (`$variableName`), and values flow through statements via a mechanism called **result propagation**. Understanding these two concepts — substitution and propagation — is essential before working with any iXML command.

## What iXML is used for

In a typical ZeyOS application, iXML code lives in three directories within the application structure. Each directory serves a different purpose, but all use the same iXML language:

- **`services/`** — API route handlers (remotecall endpoints), scheduled jobs (timing services), and entity lifecycle hooks (before/after modification triggers). Services are the entry points for all application logic.
- **`resources/`** — Reusable domain logic, shared templates, helper functions, and configuration modules. Resources are included by services and other resources using the `<include>` command.
- **`weblets/`** — Embedded UI components that render within the ZeyOS interface. Weblets generate HTML output with embedded data from the ZeyOS platform.

In practical terms, iXML typically handles:

- Receiving and parsing incoming HTTP requests (JSON, form data, XML payloads)
- Validating input against business rules
- Orchestrating database reads and writes, often within transactions
- Calling external REST and SOAP APIs
- Generating structured responses (JSON, HTML, PDF, CSV)
- Managing error conditions and returning consistent error envelopes

### See also

- [Syntax Essentials](https://www.zeyos.com/ixml/ixml-guide/syntax-essentials) — variables, strings, control flow, and the commands you use in every service
- [Reusability and OOP](https://www.zeyos.com/ixml/ixml-guide/reusability-and-oop) — functions, macros, classes, and includes for structuring larger codebases

---

## Core definitions

These definitions form the conceptual baseline used throughout the iXML reference and this guide. Every term below has a precise meaning that the language runtime enforces.

### Terminology

The iXML language specification distinguishes between the structural components of an iXML document and their semantic roles:

| Term | Meaning |
|---|---|
| **Element** | Simple XML element — a single tag with attributes and optional content |
| **Structure** | Compound XML structure containing one or more child elements with their own semantics |
| **Operation** | The semantic definition of an element — what an element *does* when it executes |
| **Construct** | The semantic definition of a structure — what a compound structure *does*, including the behavior of its children |
| **Statement** | An executable instance of an operation or construct — the concrete occurrence in your code |
| **Code** | A series of statements that execute in sequence |

The distinction between *operation* and *construct* determines whether an iXML tag stands alone or contains child elements with their own semantics. For example, `<set>` is an operation — it assigns a value and has no semantic children. In contrast, `<if>` is a construct — it can contain `<elseif>`, `<elseis>`, and `<else>` children, each with their own execution rules.

### Occurrences

Child elements within a construct are governed by occurrence rules that specify how many times a child element may appear:

| Occurrence | Meaning |
|---|---|
| `single` | The element may appear **at most once**. If it appears more than once, the behavior is undefined. |
| `multiple` | The element may appear **any number of times**, including not at all. |

### Input types

Every attribute in the iXML reference declares an input type that governs how the attribute value is interpreted at runtime. When you write an attribute value in your iXML code, the runtime does not treat all values as plain strings — instead, it processes each value according to the declared input type of that attribute.

| Type | Meaning |
|---|---|
| `var` | String referencing a variable (`name`) or an array item with a specific key (`name[key]`, `name.key`). Unnecessary whitespace characters are automatically stripped. |
| `bool` | Applicable if set and not empty. Any non-empty string in the attribute value is treated as `true`; an empty string or the absence of the attribute is treated as `false`. |
| `int` | Arithmetic expression cast to an integer number. The full expression is evaluated first, then the result is truncated to an integer. |
| `float` | Arithmetic expression cast to a floating point number. The full expression is evaluated, preserving the fractional part. |
| `date` | Arithmetic expression cast to an integer number and interpreted as a Unix timestamp. The resulting integer represents seconds since January 1, 1970 (UTC). |
| `string` | Arbitrary byte sequence or series of Unicode (UTF-8) characters. Variable substitution is performed on the value before it is passed to the command. |
| `regexp` | String interpreted as a Perl-compatible regular expression pattern enclosed in delimiters (e.g. `/[a-z]+/i`). |
| `type` | Meta type identifier (case-insensitive). Used for attributes that accept type names like `string`, `int`, `array`, etc. |
| `ixml` | Arbitrary iXML code. The content is parsed and executed as a series of statements. |

### Result types

Every operation or construct that produces a value declares a result type:

| Type | Meaning |
|---|---|
| `null` | `NULL` — the absence of a meaningful value |
| `bool` | Boolean — either `TRUE` or `FALSE` |
| `int` | Integer number — a whole number without a fractional part |
| `float` | Floating point number — a number with a fractional part |
| `string` | String — an arbitrary byte sequence or series of Unicode characters |
| `array` | Array — a collection of items in an ordered map that associates keys to values |
| `function` | Function — a closed subroutine with its own local context |
| `macro` | Macro — an open subroutine that executes in the caller's context |
| `class` | Class — an array prototype used for object-oriented programming |

### Predicates

Predicates are behavioral flags that modify how a command's result is handled by the runtime:

| Predicate | Meaning |
|---|---|
| `no-result-propagation` | The result is **not** propagated to the parent statement. |

When a command carries the `no-result-propagation` predicate, its result cannot be used inline via result propagation. For example, `<function>` and `<class>` definitions carry this predicate — they produce a value (the function or class itself), but that value is never propagated to a parent `<output>` or `<set>`. The value is only stored in the variable specified by the `var` attribute.

---

## iXML document envelope

A complete iXML document should use the standard XML envelope with the iXML namespace and the DTD reference. This is the envelope in the SDK's current default template:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ixml SYSTEM "https://developers.zeyos.com/schema/ixml.dtd">
<ixml xmlns="https://developers.zeyos.com/schema/ixml">

    <!-- iXML statements -->

</ixml>
```

Each part of the envelope serves a specific purpose:

| Part | Purpose |
|---|---|
| `<?xml ... ?>` | Standard XML declaration specifying the XML version (`1.0`) and character encoding (`UTF-8`). This must be the very first line of the file with no preceding whitespace. |
| `<!DOCTYPE ...>` | Optional DTD reference for validation. When present, the DTD enables basic structural validation of the document against the iXML element definitions. |
| `xmlns` | The iXML namespace URI. This declares that all unqualified elements in the document belong to the iXML namespace. |

When testing snippets with `zeysdk run --ixml` or the Development Console, the outer envelope can be omitted for convenience. Application files (services, resources, and weblets) should use the full envelope.

:::note
Older templates, and the envelope shown in the language reference, also declare `xsi:schemaLocation` pointing to `ixml.xsd`. Do not add it to new files. That XSD URL was reported to return 404, so use the DTD reference only.
:::

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/data-types

# Data Types

iXML is a loosely typed language where the data type of a variable is often determined by the context in which it is used. However, the underlying runtime recognizes distinct intrinsic data types, and understanding them is essential for avoiding common bugs.

## Type system overview

XML does not know intrinsic data types — in iXML, textual content is **string by default** unless converted by context or explicit cast. This is one of the most important facts for new iXML developers:

```xml
<set var="x">42</set>        <!-- "42" is a string -->
<cast var="x" type="int" />  <!-- now 42 is an integer -->
```

The `<set>` command always produces a string. If your logic depends on numeric or boolean behavior, you must cast explicitly with [`cast`](https://www.zeyos.com/ixml/ref/cast) or rely on automatic evaluation in contexts that expect a specific type (like `int` attributes).

## Available types

iXML recognizes nine types, divided into **primitive** (assigned by value) and **complex** (assigned by reference):

| Type | Category | Neutral Literal | Description |
|------|----------|----------------|-------------|
| `null` | Primitive | `NULL` | Absence of a meaningful value |
| `bool` | Primitive | `FALSE` | Truth value (`TRUE` or `FALSE`) |
| `int` | Primitive | `0` | Whole number without fractional part |
| `float` | Primitive | `0.0` | Number with fractional part |
| `string` | Primitive | `''` (empty) | Byte sequence or Unicode characters |
| `array` | Complex | — | Ordered key-value collection |
| `function` | Complex | — | Closed subroutine with local context |
| `macro` | Complex | — | Open subroutine in caller's context |
| `class` | Complex | — | Array prototype for OOP |

Use [`typeof`](https://www.zeyos.com/ixml/ref/typeof) to inspect a variable's type at runtime:

```xml
<set var="x">42</set>
<typeof var="x" var_result="t" />
<output>$t</output>
<!-- string -->
```

## Detailed type guides

- **[Strings](https://www.zeyos.com/ixml/ixml-guide/data-types/strings)** — the default type, string operations, and encoding
- **[Numbers](https://www.zeyos.com/ixml/ixml-guide/data-types/numbers)** — integers, floats, arithmetic context, and notation
- **[Booleans](https://www.zeyos.com/ixml/ixml-guide/data-types/booleans)** — truth values, truthiness rules, and comparison behavior
- **[Null and Undefined](https://www.zeyos.com/ixml/ixml-guide/data-types/null-and-undefined)** — the difference between null, undefined, and empty
- **[Arrays](https://www.zeyos.com/ixml/ixml-guide/data-types/arrays)** — ordered key-value collections, the primary complex type
- **[Type Conversion](https://www.zeyos.com/ixml/ixml-guide/data-types/type-conversion)** — casting rules, comparison behavior, and the conversion table

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/data-types/arrays

# Arrays

The **Array** type (`array`) is the primary complex data structure in iXML. It is an ordered map that associates keys to values.

- **typeof result**: `'array'`
- **Assignment**: Assigned by **reference** (not copied on assignment)

## Key features

- **Associative**: Keys can be strings (`array.key`) or integers (`array[0]`).
- **Ordered**: Iteration order is preserved.
- **Nested**: Arrays can contain other arrays, enabling complex data structures.

## Creating arrays

```xml
<!-- Inline construction -->
<array var="colors">
    <item>red</item>
    <item>green</item>
    <item>blue</item>
</array>

<!-- Incremental construction -->
<set var="person.name">John</set>
<set var="person.age">30</set>
```

[`array`](https://www.zeyos.com/ixml/ref/array)

## By-reference behavior

Arrays are assigned by reference. When you assign an array to another variable, both variables point to the same data:

```xml
<array var="original">
    <item key="a">1</item>
</array>
<assign var="copy" var_source="original" />
<set var="copy.a">changed</set>
<output>$original.a</output>
<!-- changed -->
```

Use [`clone`](https://www.zeyos.com/ixml/ref/clone) if you need an independent copy:

```xml
<clone var="independent" var_source="original" />
```

## Accessing array items

Use dot notation for known keys and bracket notation for dynamic keys:

```xml
<output>$person.name</output>        <!-- dot notation -->
<output>$person[name]</output>       <!-- bracket notation -->

<set var="key">name</set>
<output>$person[$key]</output>       <!-- dynamic key -->
```

See [Variables and Substitution](https://www.zeyos.com/ixml/ixml-guide/introduction/variables-and-substitution) for the full key chain syntax.

:::warning Destructive operations
Many array commands in iXML are **destructive by default** — they modify the source array in-place. Commands that support a `var_result` attribute let you opt into non-destructive behavior.
:::

## In this chapter

- **[Construction](https://www.zeyos.com/ixml/ixml-guide/data-types/arrays/construction)** — `array`, `item`, `array:range`, `array:assoc`, `array:populate`
- **[Inspection](https://www.zeyos.com/ixml/ixml-guide/data-types/arrays/inspection)** — `array:length`, `array:keyexists`, `array:valueexists`, `array:pos`, `array:first`, `array:last`, `array:rand`
- **[Stack and Queue](https://www.zeyos.com/ixml/ixml-guide/data-types/arrays/stack-queue)** — `array:push`, `array:pop`, `array:shift`, `array:unshift`
- **[Slicing and Restructuring](https://www.zeyos.com/ixml/ixml-guide/data-types/arrays/slicing)** — `array:concat`, `array:slice`, `array:extract`, `array:pad`, `array:reverse`, `array:flip`, `array:chunk`
- **[Set Operations](https://www.zeyos.com/ixml/ixml-guide/data-types/arrays/set-operations)** — `array:merge`, `array:unique`, `array:diff`, `array:intersect`, `array:union`
- **[Filtering and Sorting](https://www.zeyos.com/ixml/ixml-guide/data-types/arrays/filtering-sorting)** — `array:filter`, `array:keys`, `array:values`, `array:sort`

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/data-types/arrays/construction

# Array Construction

## `array` — Build array

[Full reference →](https://www.zeyos.com/ixml/ref/array)

The `ARRAY` command is the primary mechanism for constructing structured data in iXML. It creates an associative array (a collection of key-value pairs) and binds it to a variable.

### Syntax

```xml
<array var="variableName">
    <item key="key1">value1</item>
    <item key="key2">value2</item>
</array>
```

### Attributes

| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| `var` | var | Yes | — | The variable name to bind the resulting array to. |
| `key` | string | No | (next consecutive numeric key) | When used as a nested element, specifies the key in the parent array. |

### Examples

**Array with explicit string keys:**

```xml
<array var="names">
    <item key="bg">Bill Gates</item>
    <item key="sj">Steve Jobs</item>
</array>
<output>$names.bg and $names.sj are competitors!</output>
```

**Array with implicit numeric keys:**

```xml
<array var="names">
    <item>Bill Gates</item>
    <item>Steve Jobs</item>
</array>
<output>$names[0] and $names[1] are competitors!</output>
```

---

## `item` — Array item

[Full reference →](https://www.zeyos.com/ixml/ref/item)

The `ITEM` command defines a single key-value pair within an `<array>` construct.

### Syntax

```xml
<item key="keyName">value</item>
<item>value</item>
```

### Attributes

| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| `var` | var | No | — | Variable name. When specified, the item's value is also stored in this variable independently. |
| `key` | string | No | (next consecutive numeric key) | The key for this item within the parent array. |

---

## `array:range` — Create numeric range array

[Full reference →](https://www.zeyos.com/ixml/ref/array-range)

The `ARRAY:RANGE` command creates an array populated with values from a numeric range.

### Syntax

```xml
<array:range var="variableName" from="start" to="end" />
<array:range var="variableName" from="start" to="end" step="increment" />
```

### Attributes

| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| `var` | var | Yes | — | The variable name to bind the resulting array to. |
| `from` | int | Yes | — | The starting value of the range (inclusive). |
| `to` | int | Yes | — | The ending value of the range (inclusive). |
| `step` | int | No | `1` | The increment between consecutive values. |

### Example

```xml
<array:range var="range" from="6" to="10" step="2" />
<!-- Result: [6, 8, 10] -->
```

---

## `array:assoc` — Associate keys and values

[Full reference →](https://www.zeyos.com/ixml/ref/array-assoc)

The `ARRAY:ASSOC` command creates a new associative array by combining parallel arrays of keys and values.

### Syntax

```xml
<array:assoc var="result" var_keys="keysArray" var_values="valuesArray" />
```

### Example

```xml
<array var="keys"><item>bg</item><item>sj</item></array>
<array var="values"><item>Bill Gates</item><item>Steve Jobs</item></array>
<array:assoc var="names" var_keys="keys" var_values="values" />
<!-- Result: {"bg": "Bill Gates", "sj": "Steve Jobs"} -->
```

---

## `array:populate` — Import array into variable scope

[Full reference →](https://www.zeyos.com/ixml/ref/array-populate)

The `ARRAY:POPULATE` command takes all key-value pairs from an array and imports them into the local variable scope as individual variables.

### Syntax

```xml
<array:populate var="sourceArray" prefix="prefix_" />
```

### Attributes

| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| `var` | var | Yes | — | The source array. |
| `prefix` | string | No | — | A prefix prepended to each key to form the variable name. |

### Example

```xml
<array var="data"><item key="id">123</item></array>
<array:populate var="data" prefix="user_" />
<output>User ID: $user_id</output>
```

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/data-types/arrays/filtering-sorting

# Filtering and Sorting

## `array:filter` — Filter array

[Full reference →](https://www.zeyos.com/ixml/ref/array-filter)

Filters an array using an expression or function.

### Syntax

```xml
<array:filter var="array" type="key" mode="exclude">pattern</array:filter>
```

## `array:keys` — Get keys

[Full reference →](https://www.zeyos.com/ixml/ref/array-keys)

Returns all keys of an array.

### Syntax

```xml
<array:keys var="array" var_result="keys" />
```

## `array:values` — Get values

[Full reference →](https://www.zeyos.com/ixml/ref/array-values)

Returns all values of an array (re-indexed).

### Syntax

```xml
<array:values var="array" var_result="values" />
```

## `array:sort` — Sort array

[Full reference →](https://www.zeyos.com/ixml/ref/array-sort)

Sorts an array by value, key, or natural order.

### Syntax

```xml
<array:sort var="array" type="asc" />
```

### Attributes

| Name | Type | Default | Description |
|---|---|---|---|
| `var` | var | — | The array to sort. |
| `type` | type | `asc` | `asc`, `desc`, `key`, `key-desc`, `natural`, `rand`. |

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/data-types/arrays/inspection

# Array Inspection

## `array:length` — Get array length

[Full reference →](https://www.zeyos.com/ixml/ref/array-length)

Returns the number of items in an array.

### Syntax

```xml
<array:length var="arrayName" var_result="resultVar" />
```

### Example

```xml
<output>Count: <array:length var="names" /></output>
```

---

## `array:keyexists` — Check if key exists

[Full reference →](https://www.zeyos.com/ixml/ref/array-keyexists)

Checks whether a specific key exists within an array.

### Syntax

```xml
<array:keyexists var="arrayName" var_result="resultVar">keyToCheck</array:keyexists>
```

### Example

```xml
<array:keyexists var="names" var_result="exists">sj</array:keyexists>
```

---

## `array:valueexists` — Check if value exists

[Full reference →](https://www.zeyos.com/ixml/ref/array-valueexists)

Checks whether a specific value exists within an array.

### Syntax

```xml
<array:valueexists var="arrayName" var_result="resultVar">valueToFind</array:valueexists>
```

---

## `array:pos` — Find position of value

[Full reference →](https://www.zeyos.com/ixml/ref/array-pos)

Searches an array for a value and returns the **key** of the matching item.

### Syntax

```xml
<array:pos var="arrayName" var_result="resultVar">valueToFind</array:pos>
```

### Attributes

| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| `var` | var | Yes | — | The array to search. |
| `type` | type | No | `first` | `first` or `last`. |
| `offset` | int | No | — | Items to skip. |

---

## `array:first` — Get first item

[Full reference →](https://www.zeyos.com/ixml/ref/array-first)

Returns the first item of an array (key and value).

### Syntax

```xml
<array:first var="arrayName" var_key="keyVar" var_value="valueVar" />
```

---

## `array:last` — Get last item

[Full reference →](https://www.zeyos.com/ixml/ref/array-last)

Returns the last item of an array.

### Syntax

```xml
<array:last var="arrayName" var_key="keyVar" var_value="valueVar" />
```

---

## `array:rand` — Get random item

[Full reference →](https://www.zeyos.com/ixml/ref/array-rand)

Returns a randomly selected item.

### Syntax

```xml
<array:rand var="arrayName" var_key="keyVar" var_value="valueVar" />
```

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/data-types/arrays/set-operations

# Set Operations

## `array:merge` — Merge arrays

[Full reference →](https://www.zeyos.com/ixml/ref/array-merge)

Merges one or more arrays into the target array. String keys overwrite, numeric keys append.

### Syntax

```xml
<array:merge var="target" var_op1="source" />
```

## `array:unique` — Remove duplicates

[Full reference →](https://www.zeyos.com/ixml/ref/array-unique)

Removes duplicate values.

### Syntax

```xml
<array:unique var="array" />
```

## `array:diff` — Set difference

[Full reference →](https://www.zeyos.com/ixml/ref/array-diff)

Computes the difference between arrays.

### Syntax

```xml
<array:diff var="arr1" var_op1="arr2" var_result="result" />
```

## `array:intersect` — Set intersection

[Full reference →](https://www.zeyos.com/ixml/ref/array-intersect)

Computes the intersection of arrays.

### Syntax

```xml
<array:intersect var="arr1" var_op1="arr2" var_result="result" />
```

## `array:union` — Set union

[Full reference →](https://www.zeyos.com/ixml/ref/array-union)

Computes the union of arrays.

### Syntax

```xml
<array:union var="arr1" var_op1="arr2" var_result="result" />
```

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/data-types/arrays/slicing

# Slicing and Restructuring

## `array:slice` — Extract slice

[Full reference →](https://www.zeyos.com/ixml/ref/array-slice)

Extracts a portion of an array.

### Syntax

```xml
<array:slice var="arrayVar" offset="0" length="10" var_result="resultVar" />
```

## `array:concat` — Concatenate arrays

[Full reference →](https://www.zeyos.com/ixml/ref/array-concat)

Joins multiple arrays into one.

### Syntax

```xml
<array:concat var="result" var_op1="array1" var_op2="array2" />
```

## `array:extract` — Extract keys

[Full reference →](https://www.zeyos.com/ixml/ref/array-extract)

Extracts specific keys from an array into a new array.

### Syntax

```xml
<array:extract var="source" keys="id,name" var_result="result" />
```

## `array:pad` — Pad array

[Full reference →](https://www.zeyos.com/ixml/ref/array-pad)

Pads an array to a specified length with a value.

### Syntax

```xml
<array:pad var="array" size="10" value="0" />
```

## `array:reverse` — Reverse array

[Full reference →](https://www.zeyos.com/ixml/ref/array-reverse)

Reverses the order of items.

### Syntax

```xml
<array:reverse var="array" />
```

## `array:flip` — Flip keys/values

[Full reference →](https://www.zeyos.com/ixml/ref/array-flip)

Exchanges keys with their associated values.

### Syntax

```xml
<array:flip var="array" />
```

## `array:chunk` — Chunk array

[Full reference →](https://www.zeyos.com/ixml/ref/array-chunk)

Splits an array into chunks of a specific size.

### Syntax

```xml
<array:chunk var="array" size="5" var_result="chunks" />
```

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/data-types/arrays/stack-queue

# Stack and Queue Operations

:::warning Destructive
These operations modify the array in-place.
:::

## `array:push` — Append item

[Full reference →](https://www.zeyos.com/ixml/ref/array-push)

Appends a value to the end of an array. Equivalent to `$var[] = value`.

### Syntax

```xml
<array:push var="arrayName">value</array:push>
```

---

## `array:pop` — Pop item

[Full reference →](https://www.zeyos.com/ixml/ref/array-pop)

Removes and returns the last item (LIFO).

### Syntax

```xml
<array:pop var="arrayName" var_result="resultVar" />
```

---

## `array:shift` — Shift item

[Full reference →](https://www.zeyos.com/ixml/ref/array-shift)

Removes and returns the first item (FIFO). Numeric keys are re-indexed.

### Syntax

```xml
<array:shift var="arrayName" var_result="resultVar" />
```

---

## `array:unshift` — Prepend item

[Full reference →](https://www.zeyos.com/ixml/ref/array-unshift)

Prepends a value to the beginning of an array. Numeric keys are re-indexed.

### Syntax

```xml
<array:unshift var="arrayName">value</array:unshift>
```

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/data-types/booleans

# Booleans

The **Boolean** type (`bool`) represents a truth value — either `TRUE` or `FALSE`.

- **Neutral literal**: `FALSE`
- **typeof result**: `'bool'`

## Creating booleans

Booleans are typically produced by comparison operations, type checks, or explicit commands:

```xml
<!-- Explicit boolean values -->
<true var="flag" />    <!-- TRUE -->
<false var="flag" />   <!-- FALSE -->

<!-- Cast from other types -->
<set var="x">1</set>
<cast var="x" type="bool" />  <!-- TRUE -->
```

[`true`](https://www.zeyos.com/ixml/ref/true) | [`false`](https://www.zeyos.com/ixml/ref/false) | [`cast`](https://www.zeyos.com/ixml/ref/cast)

## Truthiness rules

Different values convert to boolean as follows:

| Value | Boolean equivalent |
|-------|--------------------|
| `undefined` | `FALSE` |
| `NULL` | `FALSE` |
| `FALSE` | `FALSE` |
| `TRUE` | `TRUE` |
| `0` (int) | `FALSE` |
| `0.0` (float) | `FALSE` |
| Any other number | `TRUE` |
| `''` (empty string) | `FALSE` |
| `'0'` (string zero) | `FALSE` |
| Any other string | `TRUE` |
| Any array | `TRUE` |

:::note
The string `"0"` is falsy when cast to boolean — this catches many developers off guard. Always use explicit type checking with [`is`](https://www.zeyos.com/ixml/ref/is) or [`typeof`](https://www.zeyos.com/ixml/ref/typeof) when the distinction matters.
:::

## Booleans in conditionals

The `<if>` construct compares two values with a comparison function — it does not directly evaluate boolean truthiness. To check if a variable is truthy, compare it against an empty string:

```xml
<if value1="$flag" func="!=" value2="">
    <output>flag is truthy</output>
</if>
```

For type-based branching, use [`is`](https://www.zeyos.com/ixml/ref/is):

```xml
<is var="x" type="bool">
    <output>x is a boolean</output>
</is>
```

## Boolean to string

When cast to string, `TRUE` becomes `"1"` and `FALSE` becomes `""` (empty string). This matters when outputting boolean values:

```xml
<true var="flag" />
<output>Flag: $flag</output>
<!-- Flag: 1 -->
```

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/data-types/null-and-undefined

# Null and Undefined

iXML distinguishes between three "empty" states that are often confused:

| State | typeof | Meaning |
|-------|--------|---------|
| **Undefined** | `'undefined'` | Variable has never been declared — it has neither type nor value |
| **Null** | `'null'` | Variable is defined but holds no data |
| **Empty string** | `'string'` | Variable is defined and holds a string with zero length |

## Null

The **NULL** type represents a variable that has been defined but holds no data. Set a variable to null explicitly with the [`null`](https://www.zeyos.com/ixml/ref/null) command:

```xml
<null var="x" />
<typeof var="x" var_result="t" />
<output>$t</output>
<!-- null -->
```

NULL is the typical result when a database lookup returns no rows or when an optional value is absent.

## Undefined

An **undefined** variable has never been assigned any value. It has neither type nor value:

```xml
<typeof var="neverSet" var_result="t" />
<output>$t</output>
<!-- undefined -->
```

You can make a defined variable undefined again with [`unset`](https://www.zeyos.com/ixml/ref/unset):

```xml
<set var="x">hello</set>
<unset var="x" />
<typeof var="x" var_result="t" />
<output>$t</output>
<!-- undefined -->
```

## Comparison behavior

In loose comparison, `undefined`, `NULL`, `FALSE`, and `''` (empty string) all compare equal to each other:

```xml
<if value1="$undefined_var" func="=" value2="">
    <output>undefined equals empty string</output>
</if>
<!-- undefined equals empty string -->
```

This means you cannot distinguish between these states using `<if>` alone. Use [`typeof`](https://www.zeyos.com/ixml/ref/typeof) for precise type checking:

```xml
<typeof var="x" var_result="t" />
<if value1="$t" func="=" value2="null">
    <output>x is explicitly null</output>
</if>
```

## Neutral literal behavior

When an uninitialized (undefined) variable is used in a typed context, it defaults to the **neutral literal** of that type:

- In arithmetic context: `0`
- In string context: `''`
- In boolean context: `FALSE`

This allows operations on uninitialized variables without errors, but can mask bugs if you expect a variable to have been set.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/data-types/numbers

# Numbers

iXML supports two numeric types:

1. **Integer** (`int`) — whole numbers without a fractional part.
2. **Float** (`float`) — numbers with a fractional part.

## Neutral literals

- Integer: `0`
- Float: `0.0`

## Creating numbers

Remember that `<set>` always creates strings. To create actual numeric values, use [`cast`](https://www.zeyos.com/ixml/ref/cast):

```xml
<set var="x">42</set>       <!-- string "42" -->
<cast var="x" type="int" /> <!-- integer 42 -->

<set var="y">3.14</set>       <!-- string "3.14" -->
<cast var="y" type="float" /> <!-- float 3.14 -->
```

## Automatic arithmetic evaluation

Whenever a number is explicitly expected (attributes of type `int` or `float`), the corresponding expression is automatically evaluated arithmetically. You can write expressions directly in these attribute values:

```xml
<for var="i" from="2 * 3" to="(10 + 5) / 3">
    <output>$i </output>
</for>
<!-- 6 5 4 -->
```

## Number notation

Integer numbers can be specified in multiple bases:

| Base | Prefix | Example | Decimal |
|---|---|---|---|
| Decimal (base 10) | none | `123` | 123 |
| Hexadecimal (base 16) | `0x` | `0x1A` | 26 |
| Octal (base 8) | `0` | `017` | 15 |
| Binary (base 2) | `0b` | `0b1101` | 13 |

Floating point numbers support **decimal** and **scientific E notation** (`1.5e3` = `1500.0`, `2.5e-2` = `0.025`).

:::warning
The octal prefix `0` can cause unexpected results. `010` is octal 10 = decimal `8`. Use bare decimal notation to avoid this ambiguity.
:::

## Inline arithmetic

The `$()` syntax embeds arithmetic results into string context:

```xml
<set var="birthyear">1950</set>
<output>This person is $(2025 - $birthyear) years old!</output>
<!-- This person is 75 years old! -->
```

## Numeric comparison

When both sides of an `<if>` comparison look numeric, iXML performs numeric comparison:

```xml
<if value1="0" func="=" value2="0.0">
    <output>These are numerically equal</output>
</if>
```

See [Type Conversion](https://www.zeyos.com/ixml/ixml-guide/data-types/type-conversion) for the full comparison behavior table.

## Math operations

For advanced math operations (abs, rounding, trigonometry, random, etc.), see the [Standard Library: Math & DateTime](https://www.zeyos.com/ixml/ixml-guide/standard-library/math-datetime) chapter.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/data-types/strings

# Strings

The **String** type (`string`) represents an arbitrary byte sequence or series of Unicode (UTF-8) characters. It is the most common type in iXML because all XML content is inherently textual.

- **Neutral literal**: `''` (empty string)
- **typeof result**: `'string'`

## Strings are the default

Every `<set>` operation produces a string, regardless of what the content looks like:

```xml
<set var="x">42</set>
<typeof var="x" var_result="t" />
<output>$t</output>
<!-- string -->
```

Even though `42` looks like a number, it is stored as the string `"42"`. To get an integer, use [`cast`](https://www.zeyos.com/ixml/ref/cast):

```xml
<set var="x">42</set>
<cast var="x" type="int" />
```

## Common string operations

iXML provides a comprehensive set of string manipulation commands:

| Task | Command | Example |
|------|---------|---------|
| Get length | [`length`](https://www.zeyos.com/ixml/ref/length) | `<length var="len">$str</length>` |
| Concatenate | [`concat`](https://www.zeyos.com/ixml/ref/concat) | `<concat var="result" glue=", ">$arr</concat>` |
| Lowercase | [`tolower`](https://www.zeyos.com/ixml/ref/tolower) | `<tolower var="str">$str</tolower>` |
| Uppercase | [`toupper`](https://www.zeyos.com/ixml/ref/toupper) | `<toupper var="str">$str</toupper>` |
| Trim whitespace | [`trim`](https://www.zeyos.com/ixml/ref/trim) | `<trim var="str">$str</trim>` |
| Find position | [`pos`](https://www.zeyos.com/ixml/ref/pos) | `<pos var="p" haystack="$str">needle</pos>` |
| Extract substring | [`substr`](https://www.zeyos.com/ixml/ref/substr) | `<substr var="part" from="0" length="5">$str</substr>` |
| Pad string | [`pad`](https://www.zeyos.com/ixml/ref/pad) | `<pad var="str" length="10" char="0" align="right">$str</pad>` |
| Regex match | [`match`](https://www.zeyos.com/ixml/ref/match) | `<match var="m" pattern="/\d+/">$str</match>` |
| Regex replace | [`replace`](https://www.zeyos.com/ixml/ref/replace) | `<replace var="str" pattern="/\s+/" replacement=" ">$str</replace>` |
| Split to array | [`split`](https://www.zeyos.com/ixml/ref/split) | `<split var="parts" delimiter=",">$str</split>` |

## String in comparisons

An empty string (`''`) is considered "falsy" — it compares equal to `NULL`, `FALSE`, and `undefined` in loose comparison:

```xml
<set var="x"></set>
<if value1="$x" func="=" value2="">
    <output>x is empty</output>
</if>
```

A non-empty string (even `"0"`) is truthy in boolean context, except when compared numerically. See [Type Conversion](https://www.zeyos.com/ixml/ixml-guide/data-types/type-conversion) for the full comparison table.

## Encoding and escaping

For encoding/decoding strings (Base64, URL encoding, HTML escaping, JSON, etc.), see the [Standard Library: Encoding](https://www.zeyos.com/ixml/ixml-guide/standard-library/encoding) chapter.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/data-types/type-conversion

# Type Conversion and Assignment

## Assignment Model

The way iXML assigns values depends on the type:

- **Primitive variables** (null, bool, int, float, string) are assigned **by value**. Copies are created.
- **Complex variables** (array, function, macro, class) are assigned **by reference**. Both variables point to the same data.

## Type Conversion Table

The following table shows how values behave across type inspection, comparison, and casting.

| Value | `TYPEOF` | `IF=''` | `IF!=''` | `IF='0'` | `CAST(bool)` | `CAST(int)` | `CAST(float)` | `CAST(string)` |
|---|---|---|---|---|---|---|---|---|
| `<undefined>` | `'undefined'` | true | false | false | `false` | `0` | `0` | `''` |
| `NULL` | `'null'` | true | false | false | `false` | `0` | `0` | `''` |
| `TRUE` | `'bool'` | false | true | false | `true` | `1` | `1` | `'1'` |
| `FALSE` | `'bool'` | true | false | false | `false` | `0` | `0` | `''` |
| `0` | `'int'` | false | true | true | `false` | `0` | `0` | `'0'` |
| `123` | `'int'` | false | true | false | `true` | `123` | `123` | `'123'` |
| `0.0` | `'float'` | false | true | true | `false` | `0` | `0` | `'0'` |
| `'0'` | `'string'` | false | true | true | `false` | `0` | `0` | `'0'` |
| `'abc'` | `'string'` | false | true | false | `true` | `0` | `0` | `'abc'` |
| `''` | `'string'` | true | false | false | `false` | `0` | `0` | `''` |
| `<array>` | `'array'` | false | true | false | `true` | `0` | `0` | `'[array#N]'` |

:::warning
**Numeric Comparison Trap**: `<if value1="0" func="=" value2="0.0">` is true because numeric comparison is performed when both sides look numeric.
:::

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/database-operations

# Database Operations

ZeyOS database access operates on two distinct layers:

1.  **[Object-Level Operations](https://www.zeyos.com/ixml/ixml-guide/database-operations/object-operations)**: Work with ZeyOS business entities (Contacts, Accounts, etc.). They handle ExtData, tags, and business logic automatically.
2.  **[SQL-Level Operations](https://www.zeyos.com/ixml/ixml-guide/database-operations/sql-operations)**: Specific SQL interface for complex queries, joins, and aggregations.
3.  **[Extending the Data Model](https://www.zeyos.com/ixml/ixml-guide/database-operations/extending-data-model)**: Augment entities with custom fields using ExtData, without changing the database schema.
4.  **[Tags](https://www.zeyos.com/ixml/ixml-guide/database-operations/tags)**: Label records with free-form tags for classification, filtering, and workflow routing.

## Architecture Guidelines

- **Use Object-Level Operations** for standard CRUD (Create, Read, Update, Delete) on single entities.
- **Use SQL-Level Operations** for reporting, bulk updates, or when you need specific fields/joins.

## Connection Management

By default, operations run against the ZeyOS application database. Use **[Connections](https://www.zeyos.com/ixml/ixml-guide/database-operations/connections)** to connect to external databases.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/database-operations/connections

# Connections and Raw SQL

## External database connections

Use [`db:connection`](https://www.zeyos.com/ixml/ref/db-connection) to connect to an external database. All operations inside the block run against that connection:

```xml
<db:connection server="host" dbname="db" username="user" password="pw">
    <!-- database operations run against this connection -->
</db:connection>
```

By default, all database operations run against the ZeyOS application database. You only need `db:connection` for external databases.

## Parameterized queries with `db:prepare` and `db:run`

[`db:run`](https://www.zeyos.com/ixml/ref/db-run) executes arbitrary SQL and returns the result set. **Always** use [`db:prepare`](https://www.zeyos.com/ixml/ref/db-prepare) to construct queries with dynamic values — this prevents SQL injection and ensures proper value escaping.

:::warning
Never concatenate user input directly into SQL strings. Always use `db:prepare` with parameter binding.
:::

### Basic pattern

Every `db:run` call should use a prepared statement:

```xml
<!-- Prepare the query with parameter binding -->
<db:prepare var="sql">
    <param>$userInput</param>
    <db:query>SELECT firstname, lastname FROM contacts WHERE email = ?</db:query>
</db:prepare>

<!-- Execute and retrieve results -->
<db:run var="result" type="assoc">$sql</db:run>
```

The `?` placeholder is replaced by the corresponding `<param>` value, properly escaped for the database.

### Result set formats

The `type` attribute on `db:run` controls how results are returned:

| Type | Description |
|------|-------------|
| `assoc` | Array of associative arrays (column names as keys) |
| `num` | Array of numerically indexed arrays |
| `list` | Flat array of values from the first column |
| `value` | Single scalar value (first column of first row) |
| `self` | Raw result set handle |

### Value binding vs identifier binding

`db:prepare` supports two placeholder types:

- **`?`** — Value binding. The parameter is escaped and quoted as a value. Use for data.
- **`$`** — Identifier binding. The parameter is quoted as an identifier. Use for table/column names.

```xml
<db:prepare var="sql">
    <param>contacts</param>
    <param>active</param>
    <db:query>SELECT * FROM $ WHERE status = ?</db:query>
</db:prepare>

<db:run var="result" type="assoc">$sql</db:run>
```

Here, `$` is replaced by the identifier `contacts` (quoted), and `?` is replaced by the value `active` (escaped).

### Multiple parameters

Parameters are bound in order of appearance:

```xml
<db:prepare var="sql">
    <param>$minAge</param>
    <param>$maxAge</param>
    <param>$status</param>
    <db:query>
        SELECT * FROM contacts
        WHERE age BETWEEN ? AND ?
        AND status = ?
    </db:query>
</db:prepare>

<db:run var="contacts" type="assoc">$sql</db:run>

<foreach var="contacts" var_value="contact">
    <output>$contact.firstname $contact.lastname&#10;</output>
</foreach>
```

### Write operations

`db:prepare` + `db:run` also works for INSERT, UPDATE, and DELETE:

```xml
<db:prepare var="sql">
    <param>$name</param>
    <param>$email</param>
    <db:query>INSERT INTO contacts (name, email) VALUES (?, ?)</db:query>
</db:prepare>

<db:run>$sql</db:run>
```

For structured write operations on ZeyOS entities, prefer the object-level [`db:set`](https://www.zeyos.com/ixml/ref/db-set) command instead, which handles ExtData, tags, and business logic automatically.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/database-operations/extending-data-model

# Extending the Data Model

ZeyOS has a fixed database schema with standard entities — contacts, accounts, transactions, tasks, tickets, and more — each with predefined fields. But applications often need to store additional data specific to their use case: a CRM might need a "lead score" on contacts, or a billing app might need a "payment terms" field on accounts.

**ExtData** solves this by providing a key-value overlay that augments any entity with custom fields — without altering the underlying schema. Each application can extend entity types independently, without conflicting with other applications or the core data model.

## How it works

ExtData fields are stored as key-value pairs associated with an entity record. Each field has a string key and a string value. You can read, write, and delete ExtData fields using the `extdata` namespace, or include them inline when using `db:set` for entity operations.

## Reading custom fields

Use [`extdata:get`](https://www.zeyos.com/ixml/ref/extdata-get) to retrieve custom field values from an entity:

```xml
<extdata:get var="data" entity="contacts" id="$contact_id">
    <extdata:field>company</extdata:field>
    <extdata:field>competitor</extdata:field>
</extdata:get>
<output>Company: $data.company</output>
```

If no `<extdata:field>` children are specified, all ExtData fields are returned.

## Writing custom fields

Use [`extdata:set`](https://www.zeyos.com/ixml/ref/extdata-set) to write custom field values. Supports both inline data and variable-based data:

```xml
<!-- Inline data -->
<extdata:set entity="contacts" id="$id">
    <extdata:data field="company">Microsoft</extdata:data>
    <extdata:data field="competitor">Apple</extdata:data>
</extdata:set>

<!-- From variable -->
<extdata:set entity="contacts" id="$id" var_data="data_array"/>
```

## Read-modify-write pattern

```xml
<extdata:get var="obj" entity="contacts" id="$id">
    <extdata:field>company</extdata:field>
</extdata:get>

<if value1="$obj.company" func="=" value2="Microsoft">
    <set var="obj.competitor">Apple</set>
</if>

<extdata:set entity="contacts" id="$id" var_data="obj"/>
```

## Removing custom fields

Use [`extdata:unset`](https://www.zeyos.com/ixml/ref/extdata-unset) to remove fields:

```xml
<extdata:unset entity="contacts" id="$id">
    <extdata:field>company</extdata:field>
</extdata:unset>
```

## Using ExtData with db:set

When creating or updating entities with [`db:set`](https://www.zeyos.com/ixml/ref/db-set), you can include ExtData fields inline alongside standard entity fields. This lets you set core data and custom fields in a single operation:

```xml
<db:set entity="contacts" var="newId">
    <db:data field="firstname">John</db:data>
    <db:data field="lastname">Smith</db:data>
    <extdata:data field="department">Engineering</extdata:data>
    <extdata:data field="employee_id">EMP-0042</extdata:data>
    <tags:name>Employee</tags:name>
</db:set>
```

## When to use ExtData vs. standard fields

- Use **standard fields** (via `db:data`) for data that maps to ZeyOS's built-in entity schema — `firstname`, `lastname`, `email`, `status`, etc.
- Use **ExtData** for application-specific data that doesn't exist in the core schema — user preferences, computed values, integration IDs from external systems, custom categorizations, or any domain-specific attribute your application needs.

ExtData fields are scoped to the application. Multiple apps can each define their own ExtData fields on the same entity type without interfering with each other.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/database-operations/object-operations

# Object-Level Operations

These commands work with high-level ZeyOS entities.

## `db:get` — Retrieve Entity

[Full reference →](https://www.zeyos.com/ixml/ref/db-get)

Retrieves an object by ID.

```xml
<db:get entity="contacts" id="123" var="contact">
    <db:field>firstname</db:field>
    <db:field>lastname</db:field>
</db:get>
```

Related: "[db:field reference →](https://www.zeyos.com/ixml/ref/db-field)"

## `db:set` — Create/Update Entity

[Full reference →](https://www.zeyos.com/ixml/ref/db-set)

Creates a new object or updates an existing one. Supports ExtData and Tags.

```xml
<db:set entity="contacts" var="newId">
    <db:data field="firstname">John</db:data>
    <extdata:data field="custom_field">Value</extdata:data>
    <tags:name>Customer</tags:name>
</db:set>
```

Related: "[db:data reference →](https://www.zeyos.com/ixml/ref/db-data) | [extdata:data reference →](https://www.zeyos.com/ixml/ref/extdata-data) | [tags:name reference →](https://www.zeyos.com/ixml/ref/tags-name)"

## `db:remove` — Delete Entity

[Full reference →](https://www.zeyos.com/ixml/ref/db-remove)

Deletes an object by ID.

```xml
<db:remove entity="contacts" id="123"/>
```

## `db:lookup` — Find Entity ID

[Full reference →](https://www.zeyos.com/ixml/ref/db-lookup)

Finds an ID matching criteria.

```xml
<db:lookup entity="contacts" var="id">
    <db:is field="email">test@example.com</db:is>
</db:lookup>
```

Related: "[db:is reference →](https://www.zeyos.com/ixml/ref/db-is)"

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/database-operations/sql-operations

# SQL-Level Operations

## `db:select` — Select Query

Builds and executes a SELECT query.

### Syntax

```xml
<db:select var_result="users" type="assoc">
    <db:fields>
        <db:field>firstname</db:field>
        <db:field>lastname</db:field>
    </db:fields>
    <db:table>contacts</db:table>
    <db:is field="type">user</db:is>
</db:select>
```

### Joins

Use `<db:join>` with `<db:inner>` or `<db:left>`.

```xml
<db:join>
    <db:inner table="accounts" field1="accounts.id" field2="contacts.account"/>
</db:join>
```

## `db:insert` / `db:update` / `db:delete`

Structured write operations use `db:insert`, `db:update`, and `db:delete`.

## Transactions

Wrap write operations in `db:transaction` for atomicity.

```xml
<db:transaction>
    <!-- multiple write operations -->
</db:transaction>
```

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/database-operations/tags

# Tags

Tags are free-form labels that can be attached to any ZeyOS entity record (contacts, accounts, transactions, tickets, etc.). Unlike ExtData, which stores structured key-value data, tags are simple strings used for categorization, filtering, and workflow routing. A single record can carry any number of tags, and the same tag name can appear on records across different entity types.

Common uses for tags include:

- **Classification** — labeling contacts as "VIP", "Partner", or "Prospect"
- **Workflow state** — marking records as "Needs Review" or "Approved"
- **Filtering** — selecting subsets of records in queries and exports
- **Segmentation** — grouping records for reports or bulk operations

## Adding tags

Use `tags:add` to attach one or more tags to a record. Specify the entity type and record ID, then list the tag names as `tags:name` children:

```xml
<tags:add entity="contacts" id="$contactId">
    <tags:name>VIP</tags:name>
    <tags:name>Newsletter</tags:name>
</tags:add>
```

Adding a tag that already exists on the record is a no-op — it will not create duplicates.

[`tags:add` reference →](https://www.zeyos.com/ixml/ref/tags-add)

### Tags inside `db:set`

When creating or updating records with `db:set`, you can include `tags:name` children directly alongside `db:data` and `extdata:data`:

```xml
<db:set entity="contacts" var="contactId">
    <db:data field="firstname">Ada</db:data>
    <db:data field="lastname">Lovelace</db:data>
    <tags:name>Customer</tags:name>
    <tags:name>A-Class</tags:name>
</db:set>
```

This sets the record's tags as part of the same operation. Note that `db:set` **replaces** all existing tags with the ones you specify (like `tags:reset`), whereas `tags:add` appends to the existing tag set.

## Removing tags

Use `tags:remove` to detach specific tags from a record:

```xml
<tags:remove entity="contacts" id="$contactId">
    <tags:name>Prospect</tags:name>
</tags:remove>
```

[`tags:remove` reference →](https://www.zeyos.com/ixml/ref/tags-remove)

## Replacing all tags

Use `tags:reset` to remove all existing tags and replace them with a new set:

```xml
<tags:reset entity="contacts" id="$contactId">
    <tags:name>Customer</tags:name>
    <tags:name>Active</tags:name>
</tags:reset>
```

After this call, the record has exactly two tags — regardless of what was there before.

[`tags:reset` reference →](https://www.zeyos.com/ixml/ref/tags-reset)

## Reading tags

### Listing tags on a record

Use `tags:list` to retrieve all tag names for a specific record as an array:

```xml
<tags:list var="tags" entity="contacts" id="$contactId" />

<foreach var="tags" var_value="tag">
    <output>$tag&n;</output>
</foreach>
```

[`tags:list` reference →](https://www.zeyos.com/ixml/ref/tags-list)

### Checking if tags exist

Use `tags:exists` to check whether a record has any tags at all:

```xml
<tags:exists var="hasTags" entity="contacts" id="$contactId" />
<if value1="$hasTags">
    <output>This contact has tags.</output>
</if>
```

To check for a specific tag, combine `tags:list` with an array search:

```xml
<tags:list var="tags" entity="contacts" id="$contactId" />
<array:search var="tags" var_result="found">VIP</array:search>
<if value1="$found" func="!=">
    <output>Contact is a VIP.</output>
</if>
```

[`tags:exists` reference →](https://www.zeyos.com/ixml/ref/tags-exists)

### Passing tags via variable

All tag commands accept `var_names` to supply tag names from an array variable instead of inline `tags:name` children:

```xml
<array var="tagList">
    <item>Customer</item>
    <item>Priority</item>
</array>

<tags:add entity="contacts" id="$contactId" var_names="tagList" />
```

## Filtering queries by tags

### `db:intags` — SQL-level tag filtering

Use `db:intags` inside `db:select` to filter results to records that carry specific tags:

```xml
<db:select var_result="vips" type="assoc">
    <db:fields>
        <db:field>c.firstname</db:field>
        <db:field>c.lastname</db:field>
        <db:field>c.email</db:field>
    </db:fields>
    <db:table alias="c">contacts</db:table>
    <db:intags entity="contacts" field="c.ID">
        <tags:name>VIP</tags:name>
    </db:intags>
</db:select>
```

You can also pass tag names from a variable:

```xml
<array var="filterTags">
    <item>VIP</item>
    <item>Partner</item>
</array>

<db:select var_result="results" type="assoc">
    <db:fields>
        <db:field>c.firstname</db:field>
        <db:field>c.lastname</db:field>
    </db:fields>
    <db:table alias="c">contacts</db:table>
    <db:intags entity="contacts" field="c.ID" var="filterTags" />
</db:select>
```

[`db:intags` reference →](https://www.zeyos.com/ixml/ref/db-intags)

### Tag joins — `db:innertags` and `db:lefttags`

For more complex queries, you can join on the tags table directly. Use `db:innertags` to include only records with a specific tag, or `db:lefttags` to include all records while making the tag data available for conditional logic:

```xml
<db:select var_result="customers" type="assoc">
    <db:fields>
        <db:field>c.firstname</db:field>
        <db:field>c.lastname</db:field>
    </db:fields>
    <db:table alias="c">contacts</db:table>
    <db:join>
        <db:innertags alias="t" entity="contacts" name="Customer" field="c.ID" />
    </db:join>
</db:select>
```

[`db:innertags` reference →](https://www.zeyos.com/ixml/ref/db-innertags) | [`db:lefttags` reference →](https://www.zeyos.com/ixml/ref/db-lefttags)

## Practical patterns

### Tag-based workflow routing

```xml
<tags:list var="tags" entity="tickets" id="$ticketId" />

<!-- Check if the ticket needs escalation -->
<array:search var="tags" var_result="isUrgent">Urgent</array:search>
<if value1="$isUrgent" func="!=">
    <!-- Escalation logic -->
    <tags:add entity="tickets" id="$ticketId">
        <tags:name>Escalated</tags:name>
    </tags:add>
</if>
```

### Bulk tagging from a query

```xml
<db:select var_result="overdue" type="assoc">
    <db:fields>
        <db:field>ID</db:field>
    </db:fields>
    <db:table>transactions</db:table>
    <db:is field="status">0</db:is>
    <db:is field="duedate" func="&lt;">$DATENOW</db:is>
</db:select>

<foreach var="overdue" var_value="row">
    <tags:add entity="transactions" id="$row.ID">
        <tags:name>Overdue</tags:name>
    </tags:add>
</foreach>
```

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/development-environment

# Development Environment

There are two ways to create and run iXML scripts in ZeyOS:

1. **ZeyOS Development Console** — a browser-based code editor built into ZeyOS for writing, testing, and debugging iXML directly in the platform.
2. **ZeyOS SDK** (`zeysdk`) — a command-line toolkit for local development, version control, and deployment workflows.

Both approaches execute iXML against the same ZeyOS runtime, so the language behavior is identical. The difference is in the development workflow.

**Choose the Development Console** when you want to:
- Quickly test a snippet or explore a concept
- Debug an issue on a running instance
- Make small changes without setting up a local environment

**Choose the SDK** when you want to:
- Develop full applications with multiple services and resources
- Use version control (git) for your codebase
- Work in your preferred IDE with XML/iXML schema support
- Deploy across multiple instances

## In this section

- [Development Console](https://www.zeyos.com/ixml/ixml-guide/development-environment/dev-console) — browser-based iXML editor and runner
- [SDK Workflow](https://www.zeyos.com/ixml/ixml-guide/development-environment/sdk) — command-line development, sync, and testing
- [App Structure](https://www.zeyos.com/ixml/ixml-guide/development-environment/app-structure) — how ZeyOS apps are organized
- [Coding Agents and Validation](https://www.zeyos.com/ixml/ixml-guide/development-environment/coding-agents) — reference-driven editing, XML formatting, parameter checks, and database verification
- [Execution Contexts](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts) — identity, inputs, output, and side effects for weblets, services, hooks, and `zeysdk run`
- [Minimal App](https://www.zeyos.com/ixml/ixml-guide/development-environment/minimal-app) — a small, complete app with a resource, a protected service, a weblet, and settings
- [Verification Checkpoints](https://www.zeyos.com/ixml/ixml-guide/development-environment/verification-checkpoints) — local checks, remote compilation, deployment, and runtime verification as separate steps
- [Troubleshooting](https://www.zeyos.com/ixml/ixml-guide/development-environment/troubleshooting) — symptoms, causes, confirmation steps, and fixes

Working with a coding agent? This section covers the **Build a ZeyOS app** route. The [Agent Setup](https://www.zeyos.com/guide/agent-workflows/agent-setup) page also covers **Connect an external application** and **Use ZeyOS business data**.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/development-environment/app-structure

# App Structure

A ZeyOS app is the unit of packaging, sync, and deployment. Understanding app structure is essential because iXML behavior depends on where code is executed within the app.

## Directory layout

A typical ZeyOS app contains:

```
app-name/
├── zeyos.app.json       # App metadata and identity
├── assets/              # Generic files
├── services/            # Entry points: API, background tasks, hooks
├── resources/           # Reusable domain logic and helpers
├── weblets/             # UI-linked iXML integrations
└── settings/            # Per-instance configuration
    ├── defaultsettings.json
    └── settings.<instance>.json
```

A useful mental model: **services** orchestrate requests, **resources** encapsulate logic, and **settings** provide runtime variability.

## Runtime contexts

Each entry point has its own identity, globals, and output behavior. This section is an overview. [Execution Contexts](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts) has the full matrix, and [Minimal App](https://www.zeyos.com/ixml/ixml-guide/development-environment/minimal-app) shows all the parts working together.

### Service context

Files in `services/` are invoked as runtime entry points. This is where you:

- Parse inbound payloads
- Enforce auth and guard clauses
- Call reusable resource logic
- Shape final response payloads

Services are registered in `zeyos.app.json` with a `type`. The SDK uses these uppercase names: `REMOTECALL` (HTTP API endpoint), `TIMING` (scheduled job), and the entity hooks `AFTER_CREATION`, `BEFORE_MODIFICATION`, `AFTER_MODIFICATION`, `AFTER_CREATION_MODIFICATION`, `BEFORE_DELETION`, and `AFTER_DELETION`. Older manifests with lowercase values still work, because the SDK normalizes them.

A `REMOTECALL` service can run without an authenticated caller, so "enforce auth" above is your code's job. See [REMOTECALL services](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#remotecall-services). Inside `rest:resource`, the request can hide globals, so [bind them with `use`](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#bind-globals-inside-restresource).

### Resource context

Files in `resources/` are consumed via [`include`](https://www.zeyos.com/ixml/ref/include), [`call`](https://www.zeyos.com/ixml/ref/call), or [`expand`](https://www.zeyos.com/ixml/ref/expand). This context should hold reusable logic that is independent from a specific transport boundary.

`include` loads a resource by default, and its `type` attribute selects `service` or `weblet` instead. What it returns depends on the resource's MIME type. See [Includes and JSON resources](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#includes-and-json-resources).

### Weblet context

Files in `weblets/` are linked to UI action flows. They compose resource functions and app settings to produce user-facing behavior. Weblets run in the user's browser session. That session does not authenticate a `REMOTECALL` service, so see [Weblets](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#weblets) before connecting the two.

## App settings and `APPSETTINGS`

Settings under `settings/` are the preferred place for tenant-specific values and environment toggles. Avoid hardcoding instance-specific values directly into service logic. `defaultsettings.json` ships with the app, and `settings.<instance>.json` overrides it recursively for one instance. See [Settings](https://www.zeyos.com/ixml/ixml-guide/development-environment/minimal-app#settings).

Use `APPSETTINGS` for values such as:

- External endpoint URLs
- Credentials references and key aliases, with secrets only in instance settings
- Feature toggles and rollout switches
- Integration mode flags

This allows one codebase to run against multiple instances with different behavior per instance, without branching code.

## Context switching and scope behavior

When iXML enters function/class/include/resource boundaries, it creates local execution context. Variable visibility follows the [precedence rules](https://www.zeyos.com/ixml/ixml-guide/introduction/scope-and-assignment#visibility-precedence) explained in the Introduction.

Key implications:

- Local variables are context-bound.
- Globals (`<global var="..." />`) should be explicit and rare.
- Free variables (`<use>`) should be intentional and readable.

## Recommended service-to-resource flow

```xml
<!-- services/account.api.ixml -->
<include id=".AccountController" />

<rest:server>
    <rest:resource route="/accounts/:id" method="GET">
        <!-- Query arguments become locals here. Bind globals before reading them. -->
        <use var="AUTH" />
        <use var="AccountController" />

        <!-- Reject the call unless AUTH identifies a permitted user. -->
        <is var="AUTH.user" type="empty">
            <set var="return">401</set>
            <return />
        </is>

        <call func="AccountController.getById" var="payload">
            <param name="id">$id</param>
        </call>
        <header>Content-Type: application/json</header>
        <output><encode:json var="payload" /></output>
    </rest:resource>
</rest:server>
```

This pattern keeps transport details local to `services/` while maintaining reusable, testable logic in `resources/`. The leading dot in `.AccountController` addresses a resource in the current app. It is confirmed by the platform source and used in real apps, but `ref/ixmldoc.xml` does not describe it.

## Common mistakes

- Treating `resources/` as route handlers instead of reusable modules.
- Hardcoding instance-specific values instead of using `settings/` and `APPSETTINGS`.
- Using globals as implicit data transport between unrelated contexts.
- Mixing UI-specific behavior into shared domain modules.
- Assuming a `REMOTECALL` route is protected because the platform did not reject the call.

## Related topics

- [Execution Contexts](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts)
- [Minimal App](https://www.zeyos.com/ixml/ixml-guide/development-environment/minimal-app)
- [Verification Checkpoints](https://www.zeyos.com/ixml/ixml-guide/development-environment/verification-checkpoints)
- [Troubleshooting](https://www.zeyos.com/ixml/ixml-guide/development-environment/troubleshooting)

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/development-environment/coding-agents

# Coding Agents and Native Code Validation

Coding agents work best when each change follows the same sequence: retrieve the relevant contract, adapt a verified example, review the patch, and compile it without execution. Run a behavioral test only when the task calls for one. Use [Agent Setup](https://www.zeyos.com/guide/agent-workflows/agent-setup) for Kilo Code, Pi or OpenCode and the [project instructions Markdown file](https://www.zeyos.com/ai/agent-instructions.md) to configure this workflow.

Agent Setup organizes agent work into three routes:

- **Build a ZeyOS app:** this page and the rest of this section, including [Execution Contexts](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts), [Minimal App](https://www.zeyos.com/ixml/ixml-guide/development-environment/minimal-app), [Verification Checkpoints](https://www.zeyos.com/ixml/ixml-guide/development-environment/verification-checkpoints), and [Troubleshooting](https://www.zeyos.com/ixml/ixml-guide/development-environment/troubleshooting).
- **Connect an external application:** the REST API and Client, covered in Agent Setup.
- **Use ZeyOS business data:** the `zeyos` CLI and data tools, also covered in Agent Setup.

## Choose the right evidence

| Question | Source in this repository |
| --- | --- |
| Is an iXML command or attribute supported? | `ref/ixmldoc.xml`, including prose, children, and result bindings |
| Does a Zymba method exist and what does it accept? | `ref/zymbaref.json` and `ref/zymbadoc.md` |
| Is a database identifier or relationship real? | `ref/dbref.json`, `ref/simpledb.sql`, and `ref/views.txt` |
| What does an API operation accept? | The corresponding file in `ref/openapi/` |
| What will an SDK command do? | The corresponding file in `ref/sdk/` and installed SDK help |
| Has this example run successfully? | Dated entries in `LEARN.md`, including later failures |
| How does this application pass inputs? | The calling service/resource/weblet, manifest, and application lessons |
| Which identity and globals does this code get? | [Execution Contexts](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts) |

Search for the exact symbol, then read its complete reference entry. Looking up only the command name misses defaults, result bindings, and context restrictions. Treat a conflict between the reference and a runtime observation as a compatibility issue to investigate. Do not change read-only reference material to make a generated example appear valid.

The Zymba guide tree is deferred draft material. Use its official reference and execution evidence when working on native Zymba; do not infer support from PHP or JavaScript.

## Format XML without changing behavior

Keep existing indentation and envelope conventions. For new examples, use two spaces per nesting level, double-quoted attributes, and one logical statement per line. Put long attribute lists on separate indented lines. Keep short text values on one line and use ` />` consistently for empty elements. These are editing conventions, not language requirements.

This is a readable form of the comparison fragment recorded as successful on 6 February 2026:

```xml
<if value1="3" value2="3">
  <output>This is true</output>
</if>
```

Its expected result is `This is true`. The default comparison is equality. There is no `condition` attribute.

For XML syntax inside attribute values, escape `<` as `&lt;`, `>` as `&gt;`, `&` as `&amp;`, and a double quote as `&quot;`. Preserve intentional entity references instead of double-escaping them. A comparison such as `func="&lt;"` is the documented representation of less-than. Raw `<` inside an attribute failed in the execution log.

Preserve text nodes, CDATA, and embedded HTML, JSON, or SQL. A formatter must not collapse significant spaces, move a literal across a command boundary, or rewrite an established namespace/envelope merely to satisfy a generic XML tool. Native snippets can contain multiple statements and prefixed command names; a generic namespace-aware parser may reject a fragment that the ZeyOS runtime accepts. Document-specific entities such as `&n;` also need the appropriate parser context.

### Preserve result propagation

An element can receive a result from a child statement. Literal text alongside embedded code is not ordinary string concatenation. The reference explains that these literal values are discarded, and this exact fragment returned `27` in `LEARN.md`:

```xml
<output><set>$(15 + (7.5 * 2 - 3))</set>471</output>
```

Do not reformat or rewrite that expression assuming it produces `27471`. When building text, use a documented string operation or substitution into a separate text-only statement:

```xml
<set var="name">iXML</set>
<output>Hello $name</output>
```

This exact fragment returned `Hello iXML`. `$` substitutes a value; it is not part of the declared variable name. `<set var="$name">` is invalid.

### Make types explicit

`set` content defaults to a string. The following recorded example returns `int` after an explicit cast:

```xml
<set var="test">42</set>
<cast var="test" type="int" />
<typeof var="test" var_result="t" />
<output>$t</output>
```

The formatted examples above reuse historically executed fragments; they are not new execution results for the current instance. Keep standalone test fragments distinct from complete application files, which may require an envelope and an entry-point context.

## Validate parameters and structure

For every changed command, inspect:

1. The exact command name and its documented attributes, input types, defaults, and allowed values.
2. Valid parents, children, occurrence constraints, and whether the content is text or executable iXML.
3. Result bindings and whether the result propagates to its parent.
4. Conditional requirements and restrictions described in prose.
5. The actual input contract of any called function, include, service, or hook.

The reference's attribute list is not a complete required-parameter schema. An absent default does not establish that the attribute is mandatory. Likewise, an empty OpenAPI `required` list does not prove that an API accepts an empty payload. Combine metadata with prose, application contracts, database constraints, and runtime evidence.

For example, this function and call were executed successfully in the log:

```xml
<function var="fullName">
  <set var="return">$firstname $lastname</set>
</function>
<call func="fullName" var="name">
  <param name="firstname">Ada</param>
  <param name="lastname">Lovelace</param>
</call>
<output>$name</output>
```

Expected output: `Ada Lovelace`. An agent checking required application inputs must inspect how `firstname` and `lastname` are used and what the caller promises. XML well-formedness alone cannot detect a missing business input. Exercise omitted, empty, malformed, and valid input cases where the application's behavior depends on them.

## Validate database requests

Write a short identifier map before writing the query. For each table or view, selected field, condition, sort expression, and join, record its exact name and source. Check column case, types, defaults, identity/generated flags, foreign keys, and enums. A CLI alias is not a database table name, and a UI label is not a column name.

For this recorded read example, the map is `users` → table; `u` → local alias; `u.ID` → the `ID` column on `users`:

```xml
<db:select var_result="users" type="assoc" limit="1">
  <db:fields>
    <db:field>u.ID</db:field>
  </db:fields>
  <db:table alias="u">users</db:table>
  <db:orderby>
    <db:orderfield>u.ID</db:orderfield>
  </db:orderby>
</db:select>
<output>$users[0].ID</output>
```

This returned `1` in the historical sandbox. Another instance can return a different ID or no row. The example assumes a visible user exists; application code must handle an empty result before using the first row.

`db:select` uses `var` for the generated SQL string and `var_result` for retrieved rows. `type="assoc"` requests rows with field names as keys. Capturing the SQL text is not a database validation pass, and this command is documented as generating and executing the query; do not advertise it as a dry-run facility.

For joins, follow every foreign key to its destination. `accounts.contact` references `contacts.ID`; that relationship does not imply any proposed field on `contacts` exists. Check each terminal field and expected cardinality. For native SQL, use documented query construction/preparation and distinguish escaped values from allowlisted identifiers. Never invent placeholder syntax or interpolate untrusted data directly.

Check business meaning after checking identifiers: a one-to-many join can duplicate totals, a nullable relationship can remove rows, and a page limit can make a count incomplete. Validate the expected result shape, null handling, stable ordering, and empty-result behavior.

### External requests use a different validator

The `zeyos` CLI's `describe` and `--dry-run`, and `client.schema.validate()`, catch common API mistakes before a request. Client/CLI 1.0.0 and later check complete documented FK paths and custom keys supplied through `customFields` (JavaScript) or an instance-bound `ZEYOS_CUSTOM_FIELDS_FILE` snapshot (CLI/MCP). Missing definitions fail explicitly. Confirm snapshot provenance and refresh after configuration changes; offline discovery does not query the tenant. Older releases accepted joined suffixes and custom fields unchecked, so use the matching client build. These checks do not validate native SQL or compile scripts. The curated required-create supplement covers account currency; check other required inputs against the endpoint and runtime. See [the API validation workflow](https://www.zeyos.com/guide/agent-workflows/agent-setup#prevent-invented-tables-and-fields).

## Apply the same discipline to Zymba

When local references are unavailable, use the [authoritative symbol index](https://www.zeyos.com/ai/zymba/symbols.json) and [namespace index](https://www.zeyos.com/ai/zymba/namespaces.json). Entries retain exact declarations, a source hash, and a JSON pointer into the [official reference mirror](https://www.zeyos.com/__doc/zymbaref.json). Fetch only the relevant namespace, such as [Array](https://www.zeyos.com/ai/zymba/namespace.Array.json), or the [reference working set](https://www.zeyos.com/zymba/llms-full.txt). Preserve duplicate declarations and static/instance distinctions. Read raw parameter/default/return metadata and annotations; a generated signature does not establish runtime correctness or a complete required-input contract. This reference index does not promote the deferred Zymba guide drafts.


Before calling a method, verify the class, receiver, argument order, optional/defaulted and nullable parameters, return value, and mutation behavior. A nullable parameter and an optional parameter are different concepts; read the default and signature information rather than assuming a nullable argument can be omitted.

The execution log provides useful regression cases:

| Mistake | Reference-backed behavior |
| --- | --- |
| Concatenating strings with `+` | Use `.`; `+` performs numeric addition |
| Calling `@String.length` | Use `count` or `@String.countChars` |
| Calling `@String.joinValues` | Use `@Array.joinValues` with its documented arguments |
| Checking `is array` | Arrays are objects; use the documented type system |
| Using PHP magic constants | `__FILE__`, `__LINE__`, and `__FUNCTION__` failed in the recorded runtime |

Do not replace those checks with a generic PHP/JavaScript linter. Use the ZeyOS compiler for native compilation checks and separate execution tests for behavior. Preserve the application's conventions for service parameters and object methods.

## Compile without executing

Use the SDK's compiler commands when the task asks to validate native code. Check
`zeysdk compile --help` and `zeysdk lint --help` in the installed SDK first. The
[official SDK debugging documentation](https://www.zeyos.com/sdk/debugging/)
describes both commands; the pinned `ref/openapi/sdk.json` separately documents
`PUT /compile` and `PUT /run`.

From an existing linked application directory, compile a changed asset or a
small inline fragment:

```bash
# Replace the example path with an existing application asset.
zeysdk compile services/example.ixml
zeysdk compile --ixml '<set var="name">iXML</set><output>Hello $name</output>'
zeysdk compile --zymba '$a = "Hello "; $b = "World"; echo $a . $b;'
```

These commands compile the supplied code **without executing it**. The inline
examples are based on the guide's historical snippets; this documentation update
did not submit them to the compiler. A compile result is not their printed output.

For project-level checks:

```bash
zeysdk lint
zeysdk lint --all
```

`lint` checks modified, locally present scripts selected from the application
manifest and standard settings files. `--all` checks all eligible locally present
`.ixml`, `.zy` and `.zymba` files in that set; it is not an unrestricted directory
scan. Record which files were checked. A successful exit with no eligible files
is not evidence that a changed script was validated; compile that asset directly
or correct its registration.

**Compiler-only is not offline.** Both commands require a linked application,
authentication and network access, and send source to the configured instance.
`lint --all` skips the preliminary server-state comparison, not compilation
requests. Confirm the intended instance and permission to send source; use the
existing private credentials. Neither command requires syncing or running the
application merely to obtain compiler diagnostics. Do not replace an unavailable
compiler check with `run`.

Compilation catches errors reported by the native compiler. Continue the reference
and schema checks above: a compiler pass does not prove that dynamic database
identifiers exist, inputs are complete, permissions are sufficient, or business
behavior is correct.

## Report validation precisely

| Stage | What it establishes | What remains |
| --- | --- | --- |
| Reference review | Names, signatures, result bindings, and documented constraints | Runtime/version differences and application behavior |
| Local structure/schema checks | Errors within the checker's explicit coverage | Native compilation, dynamic values and missing metadata |
| SDK `compile` / `lint` | Native compiler diagnostics for the reported files on the selected instance, without execution | Runtime inputs, database semantics, permissions and business behavior |
| Sandbox execution | Observed behavior for supplied inputs | Other inputs and invocation contexts |
| Entry-point integration test | Behavior through the intended hook, form, service, or HTTP request | Production-specific data and operational conditions |

[Verification Checkpoints](https://www.zeyos.com/ixml/ixml-guide/development-environment/verification-checkpoints) turns these stages into four steps with expected outcomes and failure cases. [Troubleshooting](https://www.zeyos.com/ixml/ixml-guide/development-environment/troubleshooting) lists common symptoms.

`zeysdk run` executes code. Use it only when execution is part of the authorized
task, with a confirmed sandbox and bounded inputs. An inline REST example in the
historical log produced HTTP 404 because the invocation did not simulate the
inbound route; compilation and inline execution do not establish route behavior.

Keep findings in the application's designated validation log with the date,
revision, reference version/hash, target instance, checker, checked files and
compiler diagnostics. For execution tests, also record inputs, expected and
actual outputs, and invocation context. Redact private data. Updating this guide's
pinned `LEARN.md` requires the separate release workflow.

If compiler access is unavailable, finish local/reference checks and report
native compilation as not performed. If execution was not requested, report it
as not performed; it need not become the default next action. Never label
unexecuted code as runtime-validated.

## Improve automation incrementally

A useful future checker can index iXML command/child contracts and Zymba method signatures from the references, resolve static database identifiers, and report source locations for each diagnostic. Dynamic names and incomplete metadata must yield a request for runtime evidence, not a false pass. This is a proposed extension, not an existing compiler supplied by the guide.

Start a regression collection with the failures above. Enable complete API relationship-path validation and supply reviewed tenant custom-field metadata before expanding unattended writes. Native SQL still requires reference and runtime checks. Preserve reference provenance and test formatting changes for result equivalence. Use restricted tools and credentials to enforce operation scope; instructions provide working guidance, not isolation from a production system.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/development-environment/dev-console

# Development Console

The ZeyOS Development Console is a browser-based code editor built into the ZeyOS platform. It allows you to write and execute iXML scripts directly on a running ZeyOS instance without installing any local tools.

## What the Development Console provides

- **Code editor** — a syntax-aware editor for writing iXML directly in the browser, with basic highlighting and validation.
- **Direct execution** — run iXML snippets against the current instance and see output immediately.
- **Debug output** — view `debug:output` and `debug:dump` messages alongside your script's regular output.
- **Quick iteration** — test ideas, debug issues, and explore the iXML API without leaving the browser.

## When to use the Development Console

The Development Console is ideal for:

- **Learning iXML** — experiment with language features and see results instantly.
- **Quick debugging** — run diagnostic queries or inspect data on a live instance.
- **Prototyping** — test a snippet before integrating it into an application.
- **Ad-hoc operations** — run one-off data queries or transformations.

For building full applications with version control and multi-instance deployment, use the [SDK](https://www.zeyos.com/ixml/ixml-guide/development-environment/sdk) instead.

## Example

A simple snippet in the Development Console:

```xml
<set var="greeting">Hello from the Development Console</set>
<output>$greeting</output>
```

Running this produces the output `Hello from the Development Console` directly in the browser.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts

# Execution Contexts

The same iXML file can behave differently depending on how it was started. Identity, available globals, and HTTP behavior all come from the **execution context**, not from the file. Most app bugs that survive compilation are context bugs: code that works in `zeysdk run` fails in a weblet, or a service that works with a token rejects a browser call.

Identify the context first, then read the row that applies. Test in that same context. A pass in one context says little about another.

## How to read the evidence labels

| Label | Meaning |
| --- | --- |
| **Reference** | Stated in the official iXML, SDK, or API reference |
| **Source-confirmed** | Read in the platform or SDK source for this release. It describes behavior at contract level and has not been executed for this page. A later platform release can change it |
| **Logged** | Dated execution result in the guide's validation log |
| **Reported** | Field observation that was not independently verified. Check it in your target |

## Context matrix

| Context | Identity and auth source | Inputs and globals | Output and HTTP | Side effects | How to test it |
| --- | --- | --- | --- | --- | --- |
| **REST API** (external client, for contrast) | Token sent by the client, or the session cookie of a signed-in browser | The documented request only. Your iXML does not run here | JSON as defined by the OpenAPI reference | Record writes can fire entity hooks | HTTP request against a sandbox, or the `zeyos` CLI |
| **Weblet, framed or page** | Browser session cookie. No valid session means no execution | `REQUEST`, `GET`, `POST`, `FILES`, `SERVER`, `SELFURL`, `VIEW`, `SELFID`, `SELFNS`, `APPSETTINGS`. Record context arrives as request parameters such as `$REQUEST.id` | Your output and headers go to the frame | No implicit transaction. Use `db:transaction` | Open the real view while signed in |
| **Weblet in a `.form` view** | Runs while the page is built for the signed-in user | `ENTITY`, `ID`, `DATA`, `EXTDATA`, `TAGS`, `FORM`, `SELFID`, `SELFNS`, `APPSETTINGS`. No `REQUEST`. New-record dialogs differ: no `FORM`, and normally no record variables | Form controls are collected. Plain output is discarded | Form data is saved by the host page | Open the record form and save it |
| **`REMOTECALL` service** | Bearer token of the access or legacy type, read into `AUTH`. The cookie is ignored. A configured access key is a separate branch | `AUTH` when authenticated, `REQUEST`, `GET`, `POST`, `FILES`, `SERVER`, `SELFURL`, `SELFID`, `SELFNS`, `APPSETTINGS`. Inside `rest:resource`: route parameters, plus query arguments or POST form fields as locals, which [can hide these globals](#bind-globals-inside-restresource) | Your output, headers, and status go to the caller. CORS is open | No implicit transaction. Unauthenticated calls can still run, even for a restricted application | HTTP request with a token, without one, with an invalid one, and with forged query arguments |
| **`zeysdk run`** | SDK token of a developer. No end-user session | `SELFID`, `SELFNS`, `APPSETTINGS`, `DEBUG`. No `AUTH`, `REQUEST`, `SELFURL`, or `VIEW` | Output, status, and headers are returned as a report | Real. Writes, mail, and external calls all happen | Only for the code path itself, on a sandbox |
| **`TIMING` service** | Scheduler. No user | `SELFID`, `SELFNS`, `APPSETTINGS` | Discarded. Errors go to the platform log | No implicit transaction. One concurrent run per service | Wait for the schedule and inspect the effects |
| **Entity hook** | Runs inside the operation that changed the record. No `AUTH` | `ENTITY`, `ID`, and [event-specific values](#entity-hooks), plus `SELFID`, `SELFNS`, `APPSETTINGS` | Discarded | Inside the record transaction. An error aborts the operation | Trigger the real entity event |

The globals, auth branches, and output handling in this table are **Source-confirmed**. Asset types and the `REMOTECALL` and `TIMING` metadata are **Reference**.

`APPSETTINGS` is the one input every application context shares. Everything else must be checked per context. The global names above are the iXML forms. Zymba has its own access syntax, so check `ref/zymbadoc.md` for Zymba assets.

## Weblets

A weblet's `type` decides how the browser reaches it. Its `view` decides where it appears. ZeySDK 3.3.0 accepts these types:

| Type | Presentation (**Source-confirmed**) |
| --- | --- |
| `INTEGRATED` | Menu entry that opens a page inside the ZeyOS shell |
| `STANDALONE` | Menu entry that navigates the same tab |
| `DETACHED` | Menu entry that opens a new tab |
| `POPUP_FRAMED`, `POPUP_PLAIN` | Menu entry that opens a dialog with a frame |
| `EMBEDDED_FRAMED`, `EMBEDDED_COLLAPSED`, `EMBEDDED_PLAIN` | Frame inside the view, with or without a collapsible heading |

All of these reach the same weblet endpoint, with one exception: a weblet registered on a view ending in `.form` is not a page. ZeyOS runs it on the server while it builds the form, so it must be iXML and it gets the record variables directly. Choose the path before you write code, because `$REQUEST` and `$ID` are not interchangeable across the two.

The endpoint requires a valid session. Without one it answers 401. An inactive weblet or application answers 403, as does a restricted application the user has no permission for. A weblet with a configured `url` redirects to that URL instead of running code.

`view` controls placement, not access. A weblet's URL is reachable by any signed-in user, regardless of its `view`, unless the application is restricted and the user lacks permission for it.

`$REQUEST.id` is an ordinary query parameter. The session proves who the user is, not that this user may see that record. Check access before you use the value in a query. See [Weblets](https://www.zeyos.com/ixml/ixml-guide/user-interface/weblets#check-record-access) for an example and for the limits on requests that change data.

## REMOTECALL services

The service URL has this form, where `<base>` is your instance URL:

```text
<base>/remotecall/<app>.<service>/<route>
```

`$SELFURL` holds the numeric form of the same address, `<base>/remotecall/~<ID>`.

These are the corrections most often needed (**Source-confirmed**, with the SDK access-key flag as **Reference**):

- **The browser session cookie does not authenticate a `REMOTECALL` call.** The platform's auth step reads only the `Authorization` header.
- **A valid token populates `AUTH`.** It holds `user`, `application`, and `lang`. Access tokens and legacy tokens are accepted. SDK and API tokens are not.
- **`AUTH.user` means any valid token.** It does not show which application the token was issued to. Compare `AUTH.application` where that matters.
- **An unauthenticated call can still execute.** A missing token and an invalid token both leave `AUTH` unset, and your script runs anyway. The platform does not answer 401 for you. Every protected handler must authorize explicitly. A restricted application does not change this: its permission check applies only when a user was identified, so an anonymous call still runs.
- **An access key is a separate branch.** If the service has an `accesskey`, the platform rejects a call without the key with 401. On success `AUTH` holds the key, not a user, so `$AUTH.user` is empty.
- **CORS is not authorization.** Every `REMOTECALL` response sends `Access-Control-Allow-Origin: *`. That header controls which pages may read a response. It does not restrict who can call the service.
- **A JSON body does not fill `REQUEST`.** Read it with `var_body` on `rest:resource` and decode it inside `try`.

### Bind globals inside `rest:resource`

Inside a handler, the request can create local variables, and locals hide globals (**Reference**). Query arguments become locals. For POST, form fields do instead (**Source-confirmed**). A global you read without binding it is therefore under the caller's control.

This matters most for authorization. Here `?AUTH[user]=1` passes the check with no token:

```xml
<!-- Wrong: a query argument can supply AUTH -->
<rest:resource route="/greeting/:name" method="GET">
  <is var="AUTH.user" type="empty">
    <set var="return">401</set>
    <return />
  </is>
</rest:resource>

<!-- Right: AUTH now refers to the platform value -->
<rest:resource route="/greeting/:name" method="GET">
  <use var="AUTH" />

  <is var="AUTH.user" type="empty">
    <set var="return">401</set>
    <return />
  </is>
</rest:resource>
```

:::warning
**The general rule:** inside `rest:resource`, any platform global (`AUTH`, `APPSETTINGS`, `REQUEST`, and so on) can be replaced by the request. Bind each one with `use` as the first child, before reading it. Do the same for your own globals, including resource controllers, macros, and shared arrays.
:::

- **A check before `rest:server` is not sufficient.** It reads the real `AUTH`, but `$AUTH.*` inside the handler can still be forged, for example `$AUTH.lang`.
- **Functions are not affected.** A function that the handler calls has its own context, so `$APPSETTINGS` inside a resource function reads the real global.
- **Route parameters win over query arguments** of the same name.
- **Any local you read before assigning it is also caller-controlled.** Assign your locals first.

This behavior was reproduced in a local run of the iXML engine with stand-in globals. Add a forged-argument test to every protected service. See [Checkpoint 4](https://www.zeyos.com/ixml/ixml-guide/development-environment/verification-checkpoints#checkpoint-4-runtime-verification).

:::warning
The `session` command reads the cookie on its own, independently of the auth step. If a browser sends the cookie to a `REMOTECALL` URL, `session` may return that user while `AUTH` stays unset. Do not build authorization on this. It has not been verified at runtime, it skips the restricted-application check, and a cookie-authorized service is exposed to cross-site request forgery.
:::

### Calling your own service from a weblet

The browser may send its cookie with the call, but the auth step ignores it, so a weblet frame that calls its own `REMOTECALL` service arrives unauthenticated. Do not paper over this by accepting anonymous calls or by shipping a long-lived token to the browser.

First ask whether the browser needs to call a service at all. The weblet already runs server-side in the user's session and can call the same resource directly, as the [minimal app](https://www.zeyos.com/ixml/ixml-guide/development-environment/minimal-app) does.

If the browser must call the service, that is a security design and it needs review. This guide does not publish a recipe. The design must define:

- **Principal binding:** which user the credential represents, set on the server from the session, never from a request parameter.
- **Route binding:** which routes, methods, and records it permits.
- **Expiry:** a short lifetime, checked on the server.
- **Replay policy:** whether reuse is allowed and how it is detected or limited.
- **Server-side key handling:** any signing key stays in instance settings and never reaches the page, the logs, or a response.
- **Verification:** tests for a missing, expired, tampered, wrong-user, and wrong-route credential, each run through a real HTTP request.

## `zeysdk run`

`zeysdk run` is console execution with application globals. It is not a simulated weblet and not an inbound HTTP request.

- It sends your **local** file to the instance and executes it there, so it can run code you have not pushed. Anything that file includes resolves to the **deployed** assets on that instance.
- It runs with the app's namespace and settings, so `$APPSETTINGS` and `.name` includes work.
- There is no session, no `AUTH`, no `REQUEST`, no `SELFURL`, and no `VIEW`. `session` returns NULL.
- Console requests use PUT, so `GET` and `POST` routes in `rest:server` do not match. **Logged:** on 6 February 2026, an inline REST example returned HTTP 404 and `null`.
- **Effects are real.** The console has no dry-run mode.

Use `run` to check a function or a query shape. Do not use it to conclude that a weblet, service route, or hook works. For compile-only checks, use `zeysdk compile`. See [Verification Checkpoints](https://www.zeyos.com/ixml/ixml-guide/development-environment/verification-checkpoints).

## TIMING services

The scheduler calls the service by `schedule` and `interval`. `schedule` is the start minute of the day, from `0` to `1439`, and `interval` is the repeat period in minutes. With an `interval` of `1`, `schedule` is ignored. The platform prevents concurrent runs of one service. All of this is **Source-confirmed**. Older `ref/sdk` pages show a `0` to `2359` time format, which the current SDK rejects. A skipped run is logged, not queued, so a job must be able to resume on its next run.

There is no user and no request. Log what the job did, because output is discarded.

## Entity hooks

Hook types are `AFTER_CREATION`, `BEFORE_MODIFICATION`, `AFTER_MODIFICATION`, `AFTER_CREATION_MODIFICATION`, `BEFORE_DELETION`, and `AFTER_DELETION`. Each needs an `entity` (**Reference**).

Inputs depend on the event (**Source-confirmed**):

| Event | Inputs besides `ENTITY` and `ID` |
| --- | --- |
| Creation | `NEW`, `NEW_EXTDATA`, `NEW_TAGS`, `MOD`, `MOD_EXTDATA`, `MOD_TAGS_ADD`. No `OLD` |
| Modification | All of the above, plus `OLD`, `OLD_EXTDATA`, `OLD_TAGS`, `MOD_TAGS_REMOVE` |
| Deletion | `OLD`, `OLD_EXTDATA`, `OLD_TAGS` only |

`AFTER_CREATION_MODIFICATION` runs for both creation and modification, so check `OLD` before using it.

A hook runs inside the transaction of the change that triggered it (**Source-confirmed**). This has three consequences:

- An uncaught error aborts the user's operation, including from an `AFTER_*` hook.
- Slow work in a hook slows every save of that entity. Move heavy work to a `TIMING` service.
- External side effects such as mail or HTTP calls are not rolled back with the transaction.

While a modification hook runs, a further change to the same record does not fire modification hooks again (**Source-confirmed**).

## Includes and JSON resources

`include` loads a resource by default. Use `type` to load a service or a weblet (**Reference**):

```xml
<include id=".Greeting" />
<include id=".api" type="service" />
<include id=".hello" type="weblet" />
```

The leading dot addresses an asset in the current app. It expands to the app identifier, so `.Greeting` in `example.minimal` means `example.minimal.Greeting`. This form is **Source-confirmed** and used throughout real apps, but `ref/ixmldoc.xml` does not describe it. It fails in a script that has no app namespace.

What a resource returns depends on its **MIME type**, not its extension (**Source-confirmed**):

| Resource MIME type | `include` returns |
| --- | --- |
| `application/ixml+xml`, `text/x-zymba` | The executed code's result |
| `application/json` | Decoded value |
| `application/xml`, `text/xml` | Parsed tree |
| `text/csv`, `text/tab-separated-values` | Parsed rows |
| Other text types | Text, with `$name` replaced from the include's parameters |

The SDK derives the MIME type from the extension when you add a file, which is why the two usually agree.

A JSON resource is already decoded, so `decode:json` on it is a mistake:

```xml
<!-- Wrong: the value is already an array -->
<include id=".defaults" var="raw" />
<decode:json var="defaults">$raw</decode:json>

<!-- Right -->
<include id=".defaults" var="defaults" />
<output>$defaults.title</output>
```

## Session locale

The `session` command returns an array, or NULL when there is no session (**Reference**). The reference does not list its keys.

The user's locale is in `data.lang` (**Source-confirmed**, and **Reported** from an app build in November 2026). A top-level `lang` key is not part of the contract, and `ref/dbref.json` lists no `lang` column on `users`.

```xml
<session var="session" />
<set var="lang">$session.data.lang</set>
```

In a `REMOTECALL` service, use `$AUTH.lang`. Always handle an empty value.

`session` returns NULL only when there is neither a session user nor a valid cookie. That makes the result context-dependent: a hook fired from a signed-in page sees that user, while the same hook fired by a `TIMING` service does not. Do not rely on `session` in hooks or in `REMOTECALL`.

## Common mistakes

- Treating a `zeysdk run` pass as proof that a weblet or service route works.
- Assuming a `REMOTECALL` handler is protected because the platform did not return 401.
- Reading `$AUTH` or `$APPSETTINGS` inside `rest:resource` without binding it with `use`.
- Treating `view` as an access control.
- Reading `$REQUEST.id` in a `.form` weblet, or `$ID` in a framed weblet.
- Sending a user-specific token to the browser to work around the missing session.
- Doing slow or external work inside a `BEFORE_*` hook.

## Related topics

- [Minimal App](https://www.zeyos.com/ixml/ixml-guide/development-environment/minimal-app)
- [Verification Checkpoints](https://www.zeyos.com/ixml/ixml-guide/development-environment/verification-checkpoints)
- [Troubleshooting](https://www.zeyos.com/ixml/ixml-guide/development-environment/troubleshooting)
- [App Structure](https://www.zeyos.com/ixml/ixml-guide/development-environment/app-structure)
- [Weblets](https://www.zeyos.com/ixml/ixml-guide/user-interface/weblets)

## Evidence

| Item | Record |
| --- | --- |
| Guide source | Revision `5271871` plus the working-tree changes of this update |
| References | `ref/ixmldoc.xml`, `ref/sdk/`, `ref/dbref.json`, `ref/openapi/sdk.json`, `LEARN.md` |
| SDK inspected | `zeysdk` 3.3.0, source and offline help |
| Checked | Static review against the references, reading the platform and SDK source, and running the `rest:resource` binding examples in a local copy of the iXML engine with stand-in globals |
| Not performed | Remote compilation, deployment, and execution on an instance. Apart from the dated log entry it cites and the local engine run, this page's claims have not been executed |

Open runtime checks are listed in `contributing/REVIEW_BACKLOG.md`.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/development-environment/minimal-app

# Minimal App

This chapter builds the smallest app that shows how the parts of a ZeyOS app fit together: one shared resource, one protected service, one weblet, and instance settings. Use it as a starting point, and as a working example of the [execution contexts](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts) rule that each entry point authenticates differently.

:::note[Verified: remote compiled on sdk-alpha, 5 November 2026]
`resources/Greeting.ixml`, `services/api.ixml` and `weblets/hello.ixml` compile with `zeysdk compile` (engine iXML 2.00-00) on a sandbox instance. They have not been deployed or run on an instance. See the [Evidence](#evidence) section before you rely on them.
:::

The app has this shape. The same files live in the guide repository under `examples/minimal-app/`.

```text
minimal-app/
├── zeyos.app.json
├── README.md
├── resources/
│   └── Greeting.ixml
├── services/
│   └── api.ixml
├── settings/
│   ├── defaultsettings.json
│   ├── settingscode.ixml
│   └── usersettingscode.ixml
└── weblets/
    └── hello.ixml
```

`resources/` holds the logic, while `services/` and `weblets/` are thin entry points that handle identity and output. The two empty settings scripts come from the scaffold.

## Create the scaffold

`zeysdk create` writes the directories, an empty `README.md`, `.zeysdk/sync.json`, and three settings files. This step is local.

```bash
zeysdk create example.minimal --name "Minimal Example" --vendor "Example"
```

The command also creates an `assets/` directory, which this app does not use. Git does not track empty directories, so it is absent from the example.

## Manifest

`zeyos.app.json` names the app and registers every asset.

```json
{
  "identifier": "example.minimal",
  "name": "Minimal Example",
  "version": 10000,
  "vendor": "Example",
  "resources": {
    "Greeting": {
      "name": "Greeting",
      "mimetype": "application/ixml+xml",
      "public": 0
    }
  },
  "services": {
    "api": {
      "name": "API",
      "type": "REMOTECALL",
      "entity": "",
      "schedule": 0,
      "interval": 1,
      "url": "",
      "mimetype": "application/ixml+xml"
    }
  },
  "weblets": {
    "hello": {
      "name": "Hello",
      "type": "EMBEDDED_FRAMED",
      "view": "contacts.details_contact.head",
      "width": 0,
      "height": 120,
      "url": "",
      "langaliases": null,
      "mimetype": "application/ixml+xml"
    }
  }
}
```

Each entry's key plus its MIME type's extension gives the file path, so `Greeting` with `application/ixml+xml` is `resources/Greeting.ixml`.

| Field | Meaning |
| --- | --- |
| `identifier` | App ID. Letters, numbers, `.`, `_`, and `-`, starting and ending with a letter or number |
| `name`, `vendor` | Display values. The SDK sends these with a push |
| `version` | Legacy field. ZeySDK 3.3.0 does not write it in a new scaffold, and a push does not send it. It stays here because the reference's manifest example includes it |
| `mimetype` | Decides how the platform treats the asset |
| `public` | `1` makes a resource publicly reachable. Keep it `0` for code |
| `type` | Service or weblet type. The SDK stores these in uppercase. Older apps with lowercase values still work, because the SDK normalizes them |
| `view` | Where a weblet appears. Valid names are in `ref/sdk/views.md` |
| `width`, `height` | Frame size in pixels for popup and embedded weblets |

### Asset identifiers

Each asset's full identifier is `<app>.<name>`. In this app they are `example.minimal.Greeting`, `example.minimal.api`, and `example.minimal.hello`. Inside the app, `.Greeting` addresses the same resource without repeating the app name. See [Includes and JSON resources](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#includes-and-json-resources) for the evidence level of this form.

### Weblet placement

`EMBEDDED_FRAMED` on `contacts.details_contact.head` shows a framed panel in the contact detail head. The example sets an explicit `height`. Automatic sizing with height `0` is [not independently verified](https://www.zeyos.com/ixml/ixml-guide/development-environment/troubleshooting#reported-not-independently-verified--check-in-your-target).

## Settings

`settings/defaultsettings.json` ships with the app and must never contain secrets.

```json
{
  "greeting": {
    "defaultPrefix": "Hello",
    "fallbackName": "there",
    "prefix": {
      "de_DE": "Hallo",
      "en_US": "Hello"
    }
  }
}
```

An optional `settings/settings.<instance>.json` holds the values for one linked instance. A push sends only the file for the active instance.

At runtime, ZeyOS merges the two recursively into `APPSETTINGS` (**Source-confirmed**). Instance values win. Nested objects merge key by key, so an instance file can override `greeting.prefix.de_DE` and keep the rest. List items merge by position rather than being replaced as a whole, so an instance list shorter than the default keeps the default's extra items.

Put secrets only in instance settings, keep those files out of version control, and never send a settings value to a browser or a log.

## Resource

The resource defines a function on a global, the same controller style real apps use. It knows nothing about HTTP or sessions, so every entry point can share it.

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ixml SYSTEM "https://developers.zeyos.com/schema/ixml.dtd">
<ixml xmlns="https://developers.zeyos.com/schema/ixml">

<global var="Greeting" />

<!--
  Build a localized greeting.

  @param string name Display name; may be empty
  @param string lang Locale such as en_US; may be empty
  @return string
-->
<function var="Greeting.forName">
  <trim var="clean">$name</trim>

  <if value1="$clean" value2="">
    <set var="clean">$APPSETTINGS.greeting.fallbackName</set>
  </if>

  <set var="prefix">$APPSETTINGS.greeting.defaultPrefix</set>

  <array:keyexists var="APPSETTINGS.greeting.prefix" var_result="known">$lang</array:keyexists>

  <is var="known" type="true">
    <set var="prefix">$APPSETTINGS.greeting.prefix[$lang]</set>
  </is>

  <set var="return">$prefix, $clean!</set>
</function>

</ixml>
```

Every iXML file here uses the envelope from the SDK's current default template, which references only the DTD.

:::warning
Do not add `xsi:schemaLocation` pointing to `ixml.xsd`. Older templates include it, but that XSD URL was reported to return 404. Only the DTD at `https://developers.zeyos.com/schema/ixml.dtd` is used here.
:::

## REMOTECALL service

The service handles one route and authorizes explicitly. An unauthenticated call still runs this file, so the check is what protects the route.

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ixml SYSTEM "https://developers.zeyos.com/schema/ixml.dtd">
<ixml xmlns="https://developers.zeyos.com/schema/ixml">

<include id=".Greeting" />

<rest:server>
  <rest:resource route="/greeting/:name" method="GET">
    <!-- Query arguments become locals here. Bind globals first so a request cannot replace them. -->
    <use var="AUTH" />
    <use var="Greeting" />

    <!-- An unauthenticated call still reaches this code. Reject it here. -->
    <is var="AUTH.user" type="empty">
      <array var="problem">
        <item key="error">Authentication required</item>
      </array>

      <set var="return">401</set>
      <header>WWW-Authenticate: Bearer</header>
      <header>Content-Type: application/json</header>
      <output><encode:json var="problem" /></output>
      <return />
    </is>

    <call func="Greeting.forName" var="text">
      <param name="name">$name</param>
      <param name="lang">$AUTH.lang</param>
    </call>

    <array var="payload">
      <item key="greeting">$text</item>
    </array>

    <header>Content-Type: application/json</header>
    <output><encode:json var="payload" /></output>
  </rest:resource>
</rest:server>

</ixml>
```

Key points:

- `$name` comes from the `:name` route segment. A route segment takes priority over a query argument of the same name.
- **`<use>` must come first.** Inside `rest:resource`, query arguments become local variables, and locals hide globals. Without `<use var="AUTH" />`, the request `?AUTH[user]=1` would create a local `AUTH` and pass the check with no token. The same applies to `Greeting`, which a request could otherwise replace to break the call.
- With `AUTH` bound, `AUTH.user` is set only for a valid token, so `empty` covers a missing token, an invalid one, and a forged query argument.
- A check placed before `rest:server` is not enough on its own, because `$AUTH.*` inside the handler could still be forged. See [Bind globals inside `rest:resource`](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#bind-globals-inside-restresource).
- Any valid token passes this check. If a route is meant for one application's tokens, also compare `AUTH.application`.
- `return` carries the HTTP status. `<return />` stops the handler.
- This check establishes **who** is calling. A route that changes data must also check **what** that user may do, and it should re-check ownership of any record it touches.
- `rest:server` buffers output, so `header` can follow other statements inside the handler. Do not produce output before `rest:server`.

This follows the [recommended service-to-resource flow](https://www.zeyos.com/ixml/ixml-guide/development-environment/app-structure#recommended-service-to-resource-flow): the service owns transport and authorization, and the resource owns the logic.

A call looks like this. The token is a user access token, not your SDK token.

```bash
curl -i \
  -H "Authorization: Bearer $ZEYOS_ACCESS_TOKEN" \
  "https://cloud.zeyos.com/<instance>/remotecall/example.minimal.api/greeting/Ada"
```

Expected result with a valid token: status 200 and a body such as `{"greeting":"Hello, Ada!"}`. Without a token, or with `?AUTH[user]=1` and no token: status 401 and `{"error":"Authentication required"}`.

## Weblet

The weblet runs in the user's session, so it reads the identity from `session` and calls the resource on the server. It does not call the `REMOTECALL` service from the browser. The browser may send its cookie with that call, but [the service's auth step ignores it](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#calling-your-own-service-from-a-weblet).

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ixml SYSTEM "https://developers.zeyos.com/schema/ixml.dtd">
<ixml xmlns="https://developers.zeyos.com/schema/ixml">

<include id=".Greeting" />

<session var="session" />

<call func="Greeting.forName" var="text">
  <param name="name">$session.name</param>
  <param name="lang">$session.data.lang</param>
</call>

<encode:html var="safeText">$text</encode:html>

<header>Content-Type: text/html; charset=utf-8</header>
<output><![CDATA[<p>$safeText</p>]]></output>

</ixml>
```

Key points:

- The text is encoded with `encode:html` before it reaches the page, because a display name is data.
- `encode:html` assigns to a variable first. Literal text next to an embedded statement in `output` [would be discarded](https://www.zeyos.com/ixml/ixml-guide/introduction/result-propagation).
- `$session.data.lang` is the session locale. The resource falls back to `defaultPrefix` if it is empty or unknown.

Expected result: a frame in the contact head showing `Hello, <your user name>!`.

## Deploy and test

Run the four [verification checkpoints](https://www.zeyos.com/ixml/ixml-guide/development-environment/verification-checkpoints) in order. In short:

```bash
zeysdk link <instance>
zeysdk compile resources/Greeting.ixml
zeysdk compile services/api.ixml
zeysdk compile weblets/hello.ixml
zeysdk push --dry-run
```

Review the plan, push to a sandbox, then test the service over HTTP and the weblet in the browser. `zeysdk run` cannot test either entry point.

## Common mistakes

- Removing the `AUTH.user` check because the service "only reads data".
- Removing `<use var="AUTH" />` because the handler seems to work without it. It does, until someone adds a query argument.
- Putting an API key in `defaultsettings.json`, which ships with the app.
- Calling the service from weblet JavaScript and expecting the session to carry over.
- Using `include` for a service or weblet without its `type`.
- Writing `<output>Hello <encode:html>$name</encode:html></output>`, which loses `Hello`.

## Related topics

- [Execution Contexts](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts)
- [Verification Checkpoints](https://www.zeyos.com/ixml/ixml-guide/development-environment/verification-checkpoints)
- [Troubleshooting](https://www.zeyos.com/ixml/ixml-guide/development-environment/troubleshooting)
- [App Structure](https://www.zeyos.com/ixml/ixml-guide/development-environment/app-structure)
- [SDK Workflow](https://www.zeyos.com/ixml/ixml-guide/development-environment/sdk)

## Evidence

| Item | Record |
| --- | --- |
| Guide source | Revision `5271871` plus the working-tree changes of this update |
| References | `ref/ixmldoc.xml`, `ref/sdk/`, `ref/sdk/views.md`, `ref/dbref.json` |
| SDK inspected | `zeysdk` 3.3.0: `create` output in a scratch directory, offline help, and source |
| Checked | Every command and attribute against `ref/ixmldoc.xml`, manifest-to-file consistency, chapter listings against the example files, JSON parsing, and XML well-formedness with `xmllint`. The service's auth check, including forged query arguments, and the weblet were also run in a local copy of the iXML engine with stand-in globals, without an instance |
| Remote compilation | `zeysdk compile` with SDK 3.3.0 on the sandbox `sdk-alpha`, 5 November 2026: all three scripts compile (engine iXML 2.00-00). The two empty settings scripts were not submitted. |
| Not performed | Deployment and execution on an instance. No runtime result on this page has been observed on an instance |

`xmllint` confirms XML well-formedness only. It is not native validation. The expected outputs above are derived from the reference and the local engine run, not observed on an instance. Open runtime checks are listed in `contributing/REVIEW_BACKLOG.md`.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/development-environment/sdk

# SDK Workflow

:::tip Full SDK Reference
For the complete command reference with all options and examples, see the **[ZeyOS SDK Documentation](https://www.zeyos.com/sdk)**.
:::

The ZeyOS SDK (`zeysdk`) is a command-line tool for developing, testing, and deploying ZeyOS applications locally. It provides a structured workflow for managing application code, synchronizing with ZeyOS instances, and validating iXML snippets.

## Command reference

| Command | Purpose | Typical frequency |
|---|---|---|
| `zeysdk create <app-id>` | Create new app scaffold | Once per app |
| `zeysdk link --token <t> --url <u> <instance>` | Bind local app to target instance | Once per instance |
| `zeysdk pull [-f]` | Fetch server state into local app | Before major edits |
| `zeysdk push` | Make the server match local state, including deletions | After verified changes |
| `zeysdk compare` | Inspect local/remote differences | Before sync |
| `zeysdk sync [-f]` | Synchronize local and remote files | Frequently |
| `zeysdk compile <path>` | Compile on the instance without executing | After each change |
| `zeysdk lint [--all]` | Compile registered scripts without executing | Before deployment |
| `zeysdk run --ixml '<snippet>'` | Execute iXML on the instance as console code | When execution is authorized |
| `zeysdk watch` | Watch and auto-sync on file changes | During active development |
| `zeysdk rollout` | Deploy to instances | For releases |

## First-time setup

```bash
mkdir -p myproject
cd myproject
zeysdk create myapp --name "My App"
cd myapp
zeysdk link --token <token> --url <url> <instanceId>
zeysdk pull -f
```

Use this once, then keep the app linked for all future development.

## Daily development loop

```bash
zeysdk compare
zeysdk sync
zeysdk run --ixml '<output>ok</output>'
```

Recommended order:

1. `compare` first to understand drift between local and remote.
2. `sync` only after reviewing expected changes.
3. `run` while editing to test snippets on a sandbox, when execution is authorized. Effects are real.

## Snippet validation

Use `zeysdk run --ixml` for fast feedback on any iXML code:

```bash
zeysdk run --ixml '<set var="name">iXML</set><output>Hello $name</output>'
```

The command returns runtime diagnostics including load time, execution time, memory usage, output size, and the output payload or any parser/runtime errors. If this fails, fix parser or language assumptions before modifying service files.

`zeysdk run` is console execution with application globals. It has no session, no `AUTH`, and no inbound request, so it does not show how a weblet or service route behaves. See [`zeysdk run`](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#zeysdk-run). To check syntax without executing, use `zeysdk compile`.

## Conflict and safety guidelines

- `pull -f` can overwrite local files — use with caution.
- `sync` can create conflict situations if both local and remote changed.
- `compare` is mandatory before any destructive or broad sync decision.
- A whole-project `push` deletes server assets that have no local copy. Preview it with `zeysdk push --dry-run`. `--keep-remote` protects only remote-only assets with no sync baseline, and `--force` skips the prompt without adding any authorization.
- SDK requests time out after 30 seconds by default. A timeout does not show that the server stopped, so check the effects before you retry.

## Verify your work

Follow the four [Verification Checkpoints](https://www.zeyos.com/ixml/ixml-guide/development-environment/verification-checkpoints): local inspection, remote compilation, deployment, and runtime verification. When something fails, start with [Troubleshooting](https://www.zeyos.com/ixml/ixml-guide/development-environment/troubleshooting). [Minimal App](https://www.zeyos.com/ixml/ixml-guide/development-environment/minimal-app) is a complete example to start from.

## Common mistakes

- Skipping `compare` and assuming only local changes exist.
- Testing only at the end of a large editing session.
- Ignoring runtime error details that already explain the fix.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/development-environment/troubleshooting

# Troubleshooting

Start from the symptom. Before changing code, identify the [execution context](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts), because the same symptom has different causes in different contexts. Evidence labels have the same meaning as on that page.

## Blank weblet

**Likely causes**

- The frame request failed with 401 or 403, or the app is inactive.
- The weblet is on a `.form` view, where plain output is discarded.
- The asset has no deployed code, so the platform returns an empty response.
- Output text was discarded by [result propagation](#text-disappears-from-output).
- A script error occurred after the frame loaded.

**How to confirm**

1. Open browser developer tools and find the request for the frame.
2. Read its status and response body. Status 200 with an empty body means the script produced nothing.
3. Check the `view` ending and the `type` in the manifest.
4. Run `zeysdk compare` to confirm that the deployed code matches yours.

**Fix**

Correct the status cause, deploy the code, or move the weblet to a view that matches how it renders. Use form commands in `.form` views and `output` elsewhere. `zeysdk run` cannot reproduce this problem, because it has no session and no frame.

## A protected route accepts a request with no token

**Likely causes**

- The handler reads `AUTH` without `<use var="AUTH" />`, so a query argument or form field supplies it.
- The check sits before `rest:server`, and the handler still reads a forgeable `$AUTH.*`.
- The handler has no check. The platform does not reject anonymous calls for you, even for a restricted application.

**How to confirm**

Send `?AUTH[user]=1` with no token, using `curl -g`. A 200 confirms the problem.

**Fix**

Add `use` as the first child of every `rest:resource`, for every global the handler reads. See [Bind globals inside `rest:resource`](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#bind-globals-inside-restresource). Treat any past use of such a route as unauthenticated.

## Unauthenticated or forbidden service call from a weblet

**Likely causes**

- The weblet's JavaScript called `REMOTECALL`, and the cookie does not authenticate that call, so `AUTH` is unset.
- The `Authorization` header holds an SDK token, not an access token.
- The application is restricted and the token's user lacks permission, which gives 403. An anonymous call is not stopped by this check and still runs.
- The service or application is inactive, which also gives 403.

**How to confirm**

Inspect the failing request in developer tools and check for an `Authorization` header. Repeat the call with `curl` and a known-good access token. If that works, the service is fine and the browser call is the problem.

**Fix**

Prefer calling the resource server-side from the weblet, as the [minimal app](https://www.zeyos.com/ixml/ixml-guide/development-environment/minimal-app#weblet) does. If the browser must call the service, follow the [design requirements](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#calling-your-own-service-from-a-weblet). Do not remove the service's check, and do not treat CORS as the cause or the fix. The response is already open to every origin.

## Wrong include target or type

**Likely causes**

- The identifier is misspelled, or its `.` prefix was dropped.
- The target is a service or weblet, but `include` defaulted to `resource`.
- The target exists locally but has not been deployed. `include` resolves on the instance.
- The script has no app namespace, so `.name` cannot resolve.

**How to confirm**

The error names the type and identifier it looked for, as written in the `include`, for example `Unable to load enhancement "resource:.api" in INCLUDE`. Compare that with the manifest. A compile pass does not check this.

**Fix**

Correct the identifier, push the target, or set the type (**Reference**):

```xml
<include id=".api" type="service" />
```

## Already-decoded JSON

**Likely causes**

The value came from an `application/json` resource, which `include` returns decoded. `decode:json` then receives an array instead of a string.

**How to confirm**

Check the resource's `mimetype` in the manifest. Inspect the value with `typeof`:

```xml
<include id=".defaults" var="defaults" />
<typeof var="defaults" var_result="kind" />
<output>$kind</output>
```

**Fix**

Delete the `decode:json`. The reverse problem also exists: a JSON **request body** arrives as a string, so read it with `var_body` and decode it inside `try`. See [Includes and JSON resources](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#includes-and-json-resources).

## Missing or empty session locale

**Likely causes**

- The code reads `$session.lang`. The locale is in `$session.data.lang` (**Source-confirmed**).
- There is no session user and no valid cookie, as in `TIMING` or `zeysdk run`.
- The code runs in a hook or in `REMOTECALL`, where the result depends on what triggered it. Do not rely on `session` there.

**How to confirm**

Check the context. `session` returns NULL without a session (**Reference**). To check the value on a sandbox, output the named key, such as `$session.data.lang`, from a weblet.

:::warning
Never dump the whole `session` array. It can include credential columns from the user record.
:::

**Fix**

Use `$AUTH.lang` in `REMOTECALL` and always provide a fallback:

```xml
<session var="session" />
<set var="lang">$session.data.lang</set>

<if value1="$lang" value2="">
  <set var="lang">en_US</set>
</if>
```

## Empty lint selection

**Symptom:** `zeysdk lint` prints `No modified iXML or Zymba files to lint.` and exits `0`.

**Likely causes**

- Nothing differs from the last synchronized state, for example straight after a push.
- The changed script is not registered in the manifest.

**How to confirm**

Run `zeysdk lint --all` and read the file list. Check that your file appears.

**Fix**

Compile the file directly with `zeysdk compile <path>`, or register it. Record an empty selection as **no files checked**, never as a pass.

## Uncertain completion after a timeout

**Symptom:** `Request timed out after 30000ms`, or a dropped connection, during `run` or `push`.

**Likely causes**

The client stopped waiting. That shows nothing about the server, which may have finished, may still be running, or may have stopped. See [Timeouts and uncertain completion](https://www.zeyos.com/ixml/ixml-guide/development-environment/verification-checkpoints#timeouts-and-uncertain-completion).

**How to confirm**

Check the effects directly: records, mail, external systems, and the platform log. After a push, run `zeysdk compare`.

**Fix**

Do not retry blindly. The SDK's request layer does not resend these requests, so a second `run` is a second execution. `zeysdk watch` does retry a failed push on its own, so stop it first. Make long work idempotent and resumable, and move it to a `TIMING` service.

## Text disappears from output

**Symptom:** `<output>` shows only part of what you wrote.

**Cause:** literal text used alongside an embedded statement is discarded (**Reference**). **Logged** on 6 February 2026: `<output><set>$(15 + (7.5 * 2 - 3))</set>471</output>` returned `27`, not `27471`.

**Fix:** assign to a variable first, then substitute it.

```xml
<!-- Wrong: "Hello" is discarded -->
<output>Hello <encode:html>$name</encode:html></output>

<!-- Right -->
<encode:html var="safeName">$name</encode:html>
<output>Hello $safeName</output>
```

See [Result propagation](https://www.zeyos.com/ixml/ixml-guide/introduction/result-propagation).

## `replace` changes nothing or the wrong text

**Cause:** in `replace`, `value` is the **search** value. The new text goes in `replacement`, and the subject is the element content (**Reference**).

```xml
<replace value="Bill Gates" replacement="William Henry Gates III">My name is Bill Gates!</replace>
```

## A route works over HTTP but returns 404 in `zeysdk run`

**Cause:** this is expected. The console has no inbound route and its requests use PUT, so `GET` and `POST` routes do not match. **Logged** on 6 February 2026: HTTP 404 and `null`.

**Fix:** test the route with an HTTP request. See [Checkpoint 4](https://www.zeyos.com/ixml/ixml-guide/development-environment/verification-checkpoints#checkpoint-4-runtime-verification).

## Reported, not independently verified — check in your target

These items come from an app build reported in November 2026. They were not reproduced for this guide, and none of them is established behavior. Test each one on your target before relying on either the report or the safe form.

| Report | What the reference says | How to check | Conservative form |
| --- | --- | --- | --- |
| After `assign`, `array:merge` on the copy also changed the source array | Complex variables are assigned by reference, and `array:merge` is destructive unless `var_result` is set | Assign, merge, then print the source | Merge with `var_result`, or use `clone` |
| JSON `false` and a missing key both read as an empty value | `is` separates `defined`, `false`, and `empty`. `array:keyexists` tests for a key | Decode `{"a":false}` and compare `$data.a` with `$data.b` | Use `array:keyexists` for presence and `is` for type |
| No rounding inside `$( )`, and empty operands break arithmetic | `math:round` exists. Uninitialized variables default to the neutral literal | Compare `math:round` with an inline expression on sample values | Use `math:round`, and default empty operands to `0` first |
| `db:is` with an empty value on an integer column fails | Not stated | On a sandbox, run a read with an empty value | Check the value with `is type="empty"` before adding the condition |
| An embedded weblet with height `0` sizes itself to its content | Not stated | Register a test weblet and resize its content | Set an explicit `height` until confirmed on your target |

## Related topics

- [Execution Contexts](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts)
- [Verification Checkpoints](https://www.zeyos.com/ixml/ixml-guide/development-environment/verification-checkpoints)
- [Minimal App](https://www.zeyos.com/ixml/ixml-guide/development-environment/minimal-app)
- [Debugging](https://www.zeyos.com/ixml/ixml-guide/error-handling/debugging)

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/development-environment/verification-checkpoints

# Verification Checkpoints

"It compiles" and "it works" are different claims. This page splits verification into four checkpoints, each with its own expected outcome. A pass at one checkpoint says nothing about the next. Record which ones you actually ran.

| Checkpoint | Needs | Proves | Does not prove |
| --- | --- | --- | --- |
| [1. Local inspection](#checkpoint-1-local-inspection) | Your files | Well-formed XML, names checked against the reference, consistent manifest | That the native compiler accepts the code |
| [2. Remote compilation](#checkpoint-2-remote-compilation) | Linked app, network, developer permission | The instance's compiler accepts the checked files | Any runtime behavior |
| [3. Deployment](#checkpoint-3-deployment) | Same, plus permission to change the instance | The instance holds the reviewed change set | That the code works in its context |
| [4. Runtime verification](#checkpoint-4-runtime-verification) | A sandbox and the real entry point | Observed behavior for the inputs you tried | Other inputs and production conditions |

## Checkpoint 1: local inspection

Nothing leaves your machine at this checkpoint.

**XML well-formedness.** This check is partial. It finds broken XML and nothing else.

```bash
xmllint --noout --nonet resources/Greeting.ixml
```

Expected outcome: exit status `0`. `xmllint` also prints `Namespace prefix ... is not defined` for commands such as `rest:server`, because iXML prefixes are not declared XML namespaces. These messages are expected. A parser error such as an unescaped `<` in an attribute is a real failure.

| Failure case | Result |
| --- | --- |
| Raw `<` in an attribute | `xmllint` fails. Escape it as `&lt;` |
| `<if condition="...">` | `xmllint` **passes**. The native parser rejects it, as logged on 6 February 2026. This is why the reference check below is required |
| Invented command such as `array:count` | `xmllint` **passes**. The native parser rejects it, as logged on 6 February 2026 |

**Reference lookup.** Look up every changed command in `ref/ixmldoc.xml`, including attributes, children, and result bindings. Check database identifiers in `ref/dbref.json` and weblet views in `ref/sdk/views.md`. [Coding Agents and Validation](https://www.zeyos.com/ixml/ixml-guide/development-environment/coding-agents#validate-parameters-and-structure) has the full checklist.

**Manifest and asset consistency.** For each manifest entry, confirm that:

- The file exists at `<class>/<key>.<extension>`, and the extension fits its `mimetype`.
- The `type` is on the SDK's list and uses the SDK's uppercase spelling.
- Hook services have an `entity`, and `TIMING` services have a `schedule` and `interval`.
- Each weblet's `view` exists in `ref/sdk/views.md`.
- Every `include` target exists in the same manifest, with the right `type` selector.
- Every `rest:resource` binds each global it reads with `use` as its first child. See [Bind globals inside `rest:resource`](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#bind-globals-inside-restresource).
- No secret appears in `defaultsettings.json` or any tracked file.

**What `zeysdk` checks offline.** `zeysdk --help`, `zeysdk <command> --help`, and `zeysdk version` work without a link. Local arguments are validated before a remote call, so `zeysdk add` and `zeysdk update` reject unknown types and unsafe paths. No SDK command compiles code offline.

## Checkpoint 2: remote compilation

`zeysdk compile` and `zeysdk lint` send your source to the linked instance's compiler and report the result. They do not execute the code, but they are **not offline**. They need a linked app, a valid token, developer permission, and network access. Confirm the instance is the one you intend, and that you may send it this source.

```bash
zeysdk whoami
zeysdk compile services/api.ixml
zeysdk compile --ixml '<output>Hello</output>'
zeysdk compile --zy 'return 1;'
zeysdk lint
zeysdk lint --all
```

`compile` takes a path or one inline source. `--zymba` is the long form of `--zy`. `lint` compiles the eligible scripts the app knows about. By default it selects only locally present scripts that differ from the last synchronized state. `--all` selects every locally present script, skipping the server comparison, but it still sends source.

Expected outcome: `compile` reports success, and `lint` prints `OK` for each file you meant to check, with a summary count. Record the file list. On failure both commands print the compiler message and exit nonzero.

| Failure case | Meaning |
| --- | --- |
| Lint prints `No modified iXML or Zymba files to lint.` or `No iXML or Zymba files found.` | **Not a pass.** Zero files were checked, and the exit status is still `0` |
| A script is missing from the lint list | It is not registered in the manifest. Register it, or compile it by path |
| `requires the zeysdk.sync.json file` or missing `id`/`url` | The app is not linked. Run `zeysdk link` |
| 401 or `Lacking developer permission` | Fix the token or the user's permission. This is not a code result |

**What compile cannot prove.** It checks the code you sent, on that instance's compiler. It does not show that database identifiers exist, that a handler's auth check cannot be forged, that an `include` target is deployed, that a settings key is present, that inputs are complete, that authorization is correct, or that the page renders. Runtime errors in dynamic values appear only at checkpoint 4.

## Checkpoint 3: deployment

`push` changes the instance. It does more than upload: a whole-project push makes the server match your working copy.

```bash
zeysdk whoami
zeysdk compare
zeysdk snapshot create "Before push"
zeysdk push --dry-run
zeysdk push
```

Expected outcome: the dry run names the application, instance, and URL, and lists every upload and every server deletion. You can explain each line. The real push then applies that plan, and a following `zeysdk compare` shows no unexpected drift.

- **Scope.** `zeysdk push services/api.ixml` plans only the named paths. Without paths the plan covers the whole project, including deletions.
- **Deletions.** A service, weblet, resource, or asset that exists on the server but not locally is deleted, even if this checkout never synchronized it.
- **`--keep-remote`** protects remote-only assets that have **no sync baseline** in this checkout. It does not protect every missing local asset. If this checkout synchronized an asset and you then removed it locally, a push still deletes it from the server.
- **`--force`** skips the confirmation prompt. It does not grant authorization, and it does not make the plan safer. Use it only after reviewing the same change set.
- **Dry run limits.** A dry run reads the server and may make read-only requests. The server does not lock, so a change made after the preview is not part of what you reviewed.
- **Snapshots** are local ZIP archives of project files. They restore your working copy, not the instance.

:::warning
Neither a snapshot nor a `sync.json` backup rolls back the server. Plan recovery before you push to a shared instance.
:::

| Failure case | Meaning |
| --- | --- |
| The plan deletes something you did not touch | Another developer's work, or a stale checkout. Stop and run `zeysdk compare`. Consider `--keep-remote` or a targeted push |
| The plan targets the wrong instance | Stop. Run `zeysdk use <instance>` |
| A push fails partway | Local recovery restores sync state only. Compare before retrying |

## Checkpoint 4: runtime verification

Test through the real entry point, on a sandbox, with bounded inputs. See [Execution Contexts](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts) for why `zeysdk run` does not substitute for these.

| Context | How to test | Expected outcome | Failure cases to try |
| --- | --- | --- | --- |
| Weblet | Open the target view in a signed-in browser. Inspect the frame request in developer tools | Frame status 200 and the expected content | Signed-out user gets 401. User without access to a restricted app gets 403. Empty and hostile `$REQUEST` values stay safe |
| `REMOTECALL` | Send HTTP requests with `curl` | Valid access token gets 200 and the expected body | No token, an invalid token, an SDK token, and no token with a forged `?AUTH[user]=1` must all be rejected **by your handler**. A real token with forged arguments must keep its real identity. A 200 on any rejected case is a security failure |
| `TIMING` | Let the schedule fire on a sandbox, then inspect the data and log | One run, with expected effects | A rerun does not repeat completed work. A failed run is logged and the next run recovers |
| Entity hook | Create, change, or delete a real record through the UI or API | The expected change, and only for the intended records | A thrown error aborts the save. Non-matching records are untouched |

The three HTTP checks every protected service needs:

```bash
# Expected: 200
curl -i -H "Authorization: Bearer $ZEYOS_ACCESS_TOKEN" \
  "https://cloud.zeyos.com/<instance>/remotecall/example.minimal.api/greeting/Ada"

# Expected: 401 from your handler
curl -i \
  "https://cloud.zeyos.com/<instance>/remotecall/example.minimal.api/greeting/Ada"

# Expected: 401. A 200 means a request can forge AUTH.
curl -i -g \
  "https://cloud.zeyos.com/<instance>/remotecall/example.minimal.api/greeting/Ada?AUTH[user]=1"
```

`-g` stops `curl` from treating the brackets as a pattern. For a `POST` route, send the same field in the form body.

### Timeouts and uncertain completion

SDK requests time out after 30 seconds by default. Set `ZEYSDK_REQUEST_TIMEOUT_MS` to a positive integer to change this. The limit applies to the client's wait.

A disconnected client does not prove the server stopped. The script may still be running, or may already have finished. The SDK's request layer does not automatically resend a `run`, `compile`, or push request, and retries only reads. `zeysdk watch` is the exception: it keeps a failed batch and pushes it again with backoff, so stop it before you investigate. Outside `watch`, any repeat comes from you.

Before you retry after a timeout:

1. Treat the outcome as unknown, not failed.
2. Check the effects: records, sent mail, external calls, and the platform log.
3. Retry only what did not happen, and only if the operation is safe to repeat.

For work that may take longer, use a `TIMING` service with a resumable, time-bounded batch instead of raising the timeout.

## Record what you ran

Keep the date, revision, instance, checked file list, and result for each checkpoint. Mark any checkpoint you skipped as **not performed**. Never describe compiled code as runtime-validated.

## Related topics

- [Execution Contexts](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts)
- [Minimal App](https://www.zeyos.com/ixml/ixml-guide/development-environment/minimal-app)
- [Troubleshooting](https://www.zeyos.com/ixml/ixml-guide/development-environment/troubleshooting)
- [SDK Workflow](https://www.zeyos.com/ixml/ixml-guide/development-environment/sdk)
- [Coding Agents and Validation](https://www.zeyos.com/ixml/ixml-guide/development-environment/coding-agents)

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/error-handling

# Error Handling & Debugging

Defensive coding in iXML is essential because services usually handle untrusted input, communicate with external dependencies, and orchestrate database side effects that can fail at any point. Without proper error handling, a single malformed JSON payload or a database constraint violation can crash an entire service, returning raw internal error messages to the client and leaving data in an inconsistent state.

iXML provides a structured error handling mechanism through the `TRY` construct and explicit error/warning commands, combined with a `debug` namespace for development-time inspection. This section covers the full error handling and debugging toolkit.

## Sub-chapters

- [Try / Catch](https://www.zeyos.com/ixml/ixml-guide/error-handling/try-catch) — The `try`/`catch`/`else`/`finally` construct, execution flow, and practical examples
- [Throwing Errors](https://www.zeyos.com/ixml/ixml-guide/error-handling/throwing-errors) — The `error` and `warning` commands for explicitly raising exceptions
- [API Error Patterns](https://www.zeyos.com/ixml/ixml-guide/error-handling/api-error-patterns) — Stable response envelopes, HTTP status codes, and implementation patterns for ZeyOS API services
- [Debugging & Troubleshooting](https://www.zeyos.com/ixml/ixml-guide/error-handling/debugging) — Parser errors, runtime errors, debug commands, and systematic debugging strategies
- [Best Practices](https://www.zeyos.com/ixml/ixml-guide/error-handling/best-practices) — Guidelines and common mistakes for robust error handling

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/error-handling/api-error-patterns

# API Error Patterns

## Stable API error contract

When building ZeyOS services that expose APIs, establish a consistent response envelope that clients can rely on. Every endpoint should return the same top-level structure regardless of whether the operation succeeded or failed. This reduces client-side complexity and makes services easier to monitor and debug.

### Recommended response shapes

**Success response:**

```json
{
    "result": 1,
    "data": { ... }
}
```

**Error response:**

```json
{
    "error": "Human-readable error message",
    "details": "Technical details for debugging"
}
```

### Implementation pattern

```xml
<array var="RES" />

<try>
    <!-- Business logic here -->
    <set var="RES.result">1</set>

    <catch var="err">
        <set var="RES.error">Operation failed</set>
        <set var="RES.details">$err</set>
    </catch>
</try>

<header>Content-Type: application/json</header>
<output><encode:json var="RES" /></output>
```

This pattern ensures that:

- The client always receives valid JSON, never a raw error dump
- The top-level key (`result` vs. `error`) immediately signals success or failure
- Technical error details are available for debugging without leaking sensitive internals
- The HTTP `Content-Type` header is always set correctly, even on error

### HTTP status codes

Use the `status` attribute of the `<header>` command to set appropriate HTTP status codes in error scenarios:

```xml
<catch var="err">
    <header status="400" />
    <set var="RES.error">Bad request: $err</set>
</catch>
```

Common status codes for API services:

| Code | Meaning | When to use |
|---|---|---|
| `200` | OK | Successful operations (default) |
| `201` | Created | Successful resource creation |
| `400` | Bad Request | Invalid input, malformed JSON, missing required fields |
| `401` | Unauthorized | Missing or invalid authentication |
| `403` | Forbidden | Authenticated but insufficient permissions |
| `404` | Not Found | Requested resource does not exist |
| `500` | Internal Server Error | Unexpected failures, database errors |

A `401` or `403` check inside `rest:resource` must [bind `AUTH` with `use`](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#bind-globals-inside-restresource), or a query argument can forge it.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/error-handling/best-practices

# Best Practices

## Best practices

- **Fail early at input boundaries.** Validate incoming data immediately after decoding it. Throw explicit `<error>` messages for missing or invalid fields before processing begins.
- **Always include a `<catch>` block** in any `<try>` that wraps code interacting with external systems (databases, APIs, file I/O). Unhandled errors from these sources produce raw technical messages that should never reach the client.
- **Use `<finally>` for response formatting.** Place your `<header>` and `<output>` statements in the `<finally>` block to ensure the response is always sent, even when errors occur.
- **Include enough error detail for troubleshooting** but avoid leaking sensitive internals. The `details` field should help a developer reproduce the issue, not expose database schemas or internal paths.
- **Keep success and failure code paths equally explicit.** Use the `<else>` block for success-only logic rather than assuming "no error means success" at the end of the `<try>` body.
- **Return consistent error shapes.** Every service should use the same JSON envelope structure for errors. Inconsistent error formats create client-side complexity.
- **Wrap system boundaries in try/catch.** JSON parsing, HTTP requests, database operations, and any external service calls should always be protected.

## Common mistakes

- **Letting parser/runtime errors bubble to the client unchanged.** Without `<try>/<catch>`, a `<decode:json>` failure produces a raw "Syntax error" message as the entire response — no JSON wrapper, no HTTP status code, no content type header.
- **Returning different error shapes per endpoint.** This forces every client to handle errors differently for each API call.
- **Swallowing exceptions without recording context.** An empty `<catch>` block hides failures, making debugging extremely difficult.
- **Placing output statements after the `<try>` block instead of in `<finally>`.** If an error occurs and the `<catch>` block doesn't produce output, the client receives an empty response.
- **Setting HTTP status and response payload inconsistently.** For example, returning a `200` status with an error payload, or a `500` status with a success payload.
- **Using `<error>` for control flow instead of `<if>/<return>`.** While `<error>` works for early exits, it incurs the overhead of exception handling. For expected conditions (like "record not found"), use `<if>` with `<return>` instead.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/error-handling/debugging

# Debugging & Troubleshooting

Troubleshooting iXML is faster when you follow a strict reduction workflow: isolate, reproduce, inspect, and fix. This page provides systematic processes for parser errors, runtime exceptions, and data-shape bugs, along with the full `debug` namespace for development-time inspection.

## Debugging Workflow

1. **Reproduce** the issue with a minimal snippet using `zeysdk run --ixml`.
2. **Capture** the exact error message and location.
3. **Reduce** by removing branches and variables until the root cause is obvious.
4. **Fix** the minimal case, then rebuild toward the full version.
5. **Verify** the fix works in the original context.

## Parser Errors

Parser errors occur before execution. The iXML engine validates XML structure, element names, and attribute names during parsing. Note that error messages show attribute and element names in uppercase — iXML itself is case-insensitive.

### Unknown Attribute

```
ERROR: iXML\ParserException: Unknown attribute "CONDITION" in element "IF"
```

**Cause:** Using an attribute that does not exist on the element.

**Common cases:**
- `<if condition="...">` — Use `value1`, `value2`, `func` instead.
- `<set var="$name">` — Remove the `$`; it is substitution syntax, not part of variable names.
- `<encode:base64 type="mime">` — `type` attribute does not exist on `encode:base64`.

**Fix:** Check the command's attribute list in the documentation.

### Malformed XML

```
ERROR: iXML\ParserException: expected '>'
```

**Cause:** Invalid XML syntax — unescaped characters in attributes, missing closing tags, or mismatched elements.

**Common cases:**
- `func="<"` — Must be `func="&lt;"` (XML requires escaping `<` and `>` in attribute values).
- Missing closing tag or mismatched nesting.

### Unknown Element

```
ERROR: iXML\ParserException: Unknown element "DB:ISNOT"
```

**Cause:** Using a command that does not exist.

**Common cases:**
- `<db:isnot>` — Does not exist. Use `<db:is func="!=">` or `<db:isnotnull/>`.
- `<encode:hex>` — Does not exist in the encode namespace.
- `<encode:hmac>` — Does not exist. Use `<encode:hash algorithm="sha256" key="secret">`.
- `<array:count>` — Does not exist. Use `<size var="count">$array</size>`.

## Runtime Errors

Runtime errors occur during execution. They can be caught with `<try>/<catch>`.

### Type Errors

**Symptom:** Unexpected behavior or empty output.

**Diagnosis:**

```xml
<set var="test">42</set>
<typeof var="test" var_result="t"/>
<output>Type: $t</output>
<!-- Output: Type: string -->
```

**Fix:** Use `<cast>` for explicit type conversion:

```xml
<cast var="test" type="int"/>
<typeof var="test" var_result="t"/>
<output>Type: $t</output>
<!-- Output: Type: int -->
```

:::tip
**Common mistake:** `<set>` always creates strings. `<set var="x">42</set>` creates the string `"42"`, not the integer `42`. Use `<cast var="x" type="int"/>` after setting if you need a numeric type.
:::

### Data Shape Inspection

When output is unexpected, serialize intermediate structures to see their actual shape:

```xml
<encode:json var="payload" var_result="debug" pretty="1"/>
<output>$debug</output>
```

Use this before database writes and before final API output to verify the data structure.

### Database Errors

**Common issues:**
- **Type mismatch:** `contacts.type` is `smallint`, not a string. Using `<db:is field="c.type">Customer</db:is>` causes a PostgreSQL type error.
- **NULL comparison:** `<db:is field="c.email">NULL</db:is>` does not check for NULL. SQL NULL requires `IS NULL` syntax — use `<db:isnull field="c.email"/>`.
- **Access forbidden:** System tables like `information_schema.columns` are blocked in ZeyOS.

### Defensive Error Handling

Wrap risky operations in `<try>/<catch>`:

```xml
<try>
    <decode:json var="data">$body</decode:json>
<catch var="err">
    <set var="return">400</set>
    <output>Invalid JSON: $err</output>
</catch>
</try>
```

## Debug Commands

The `debug` namespace provides four commands for development-time inspection. The command descriptions below cover the available debug operations.

### debug:output

[Full reference →](https://www.zeyos.com/ixml/ref/debug-output)

Writes a message to the debug console (separate from application output — you will not see it in `zeysdk run --ixml` results):

```xml
<debug:output>Processing ID: $id, Status: $status</debug:output>
```

### debug:dump

[Full reference →](https://www.zeyos.com/ixml/ref/debug-dump)

Dumps all variables or a specific variable with recursive structure inspection:

```xml
<!-- Dump a specific variable -->
<debug:dump var="contacts"/>

<!-- Dump all variables -->
<debug:dump/>
```

### debug:log

[Full reference →](https://www.zeyos.com/ixml/ref/debug-log)

Writes to the debug log file (persisted, unlike console output):

```xml
<debug:log>Error processing contact $id: $error_msg</debug:log>
```

### debug:exclude

[Full reference →](https://www.zeyos.com/ixml/ref/debug-exclude)

Temporarily disables a code block without removing it. Unlike XML comments, the code inside is still parsed and validated — so syntax errors in disabled code will still be caught:

```xml
<debug:exclude>
    <db:delete entity="contacts">
        <db:is field="c.status">inactive</db:is>
    </db:delete>
</debug:exclude>
```

## Debugging Strategies

### Service Endpoint Debugging

When debugging API handlers, check in this order:

1. **Route and method** — Is the correct service being invoked?
2. **Body decode** — Is the request body valid and parsed correctly?
3. **Validation** — Are required fields present and correctly typed?
4. **Database operations** — Are queries returning expected results?

This order prevents misdiagnosing downstream failures caused by upstream issues.

### Iterative Reduction

When facing a complex bug, reduce the code iteratively:

```xml
<!-- Step 1: Does the basic structure work? -->
<output>Step 1: Start</output>

<!-- Step 2: Does the data load correctly? -->
<db:select var="data" entity="contacts" type="self" limit="1">
    <db:fields>c.ID, c.name</db:fields>
</db:select>
<encode:json var="data" var_result="json" pretty="1"/>
<output>$json</output>

<!-- Step 3: Add processing logic back one piece at a time -->
```

### Regression Prevention

After fixing a bug:

1. Keep a minimal reproducer snippet.
2. Run it again and capture the passing output.
3. Document the issue to avoid repeating the same mistake.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/error-handling/throwing-errors

# Throwing Errors

## `error` — Throw error

The `ERROR` command explicitly throws an error, breaking out of the current control flow. The error propagates upward until it is caught by a `<try>/<catch>` block. If no `<try>/<catch>` block is present, the error terminates execution entirely.

The content of the `<error>` element becomes the error message string that is received by the `<catch>` block's `var` attribute. You can use variable substitution in the error message to include dynamic context information.

[Full reference →](https://www.zeyos.com/ixml/ref/error)

### Syntax

```xml
<error>Error message text with $variable substitution</error>
```

The content (type: string) is the error message.

### When to use explicit errors

Use `<error>` to enforce business rules, validate preconditions, or signal application-level failures that the caller should handle. For expected conditions (like "record not found"), prefer `<if>` with `<return>` instead — `<error>` incurs exception-handling overhead and should be reserved for truly exceptional situations.

```xml
<is var="payload.email" type="empty">
    <error>Email address is required</error>
</is>

<if value1="$payload.amount" func="&lt;" value2="0">
    <error>Amount must be non-negative, got: $payload.amount</error>
</if>
```

### Example

```xml
<try>
    <set var="name">iXML</set>

    <if value1="$name" func="=" value2="iXML">
        <error>An error has occurred!</error>
    </if>

    <catch var="error">
        <output>$error</output>
    </catch>
</try>

<!-- An error has occurred! -->
```

### See also

- [Try / Catch](https://www.zeyos.com/ixml/ixml-guide/error-handling/try-catch) — catching errors
- [`warning`](#warning--throw-warning) — the non-fatal alternative

---

## `warning` — Throw warning

The `WARNING` command throws a warning, which behaves similarly to an error: it breaks out of the current control flow and can be caught by a `<try>/<catch>` block. Warnings are a ZeyOS-specific mechanism for signaling non-critical issues that should be logged or reported but may not necessarily require the same handling as a full error.

[Full reference →](https://www.zeyos.com/ixml/ref/warning)

### Syntax

```xml
<warning>Warning message text</warning>
```

The content (type: string) is the warning message.

:::note
Like errors, warnings are caught by `<try>/<catch>`. The `<catch>` block receives the warning message in its `var` attribute, just as it would for an error. There is no built-in way to distinguish between caught errors and caught warnings at the `<catch>` level — if you need to differentiate, use a convention in your message format or set a flag variable before throwing.
:::

### Example

```xml
<warning>This is a warning!</warning>
```

### See also

- [`error`](#error--throw-error) — the standard error-throwing mechanism
- [Try / Catch](https://www.zeyos.com/ixml/ixml-guide/error-handling/try-catch) — catching warnings

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/error-handling/try-catch

# Try / Catch

## `try` — Error handling construct

The `TRY` construct is the primary control flow structure for handling errors, warnings, exceptions, and other special conditions that change the normal execution flow. It establishes a **protected region** of code: if any statement within the `TRY` block throws an error (either explicitly with `<error>` or implicitly from a runtime failure), execution immediately jumps to the `<catch>` block instead of terminating the entire service.

The `TRY` construct supports up to four sections, each with distinct execution semantics:

1. **The `try` body** — the protected code that might fail
2. **`catch`** — code that runs only when an error occurs
3. **`else`** — code that runs only when no error occurs
4. **`finally`** — code that always runs, regardless of whether an error occurred

This four-part structure gives you precise control over both the success and failure paths of any operation, while ensuring that cleanup code (like closing resources or sending response headers) always executes.

[Full reference →](https://www.zeyos.com/ixml/ref/try)

### Syntax

```xml
<try>
    <!-- Protected code that might throw an error -->
    <set var="data">some operation</set>

    <catch var="errorMessage">
        <!-- Runs ONLY if an error was thrown in the try body -->
        <output>Error: $errorMessage</output>
    </catch>

    <else>
        <!-- Runs ONLY if NO error was thrown in the try body -->
        <output>Success</output>
    </else>

    <finally>
        <!-- ALWAYS runs, regardless of success or failure -->
        <output> (done)</output>
    </finally>
</try>
```

### Child element: `catch` — Error-triggered execution

Occurrence: "**single**"

The `<catch>` block executes **only** when an error is thrown within the `<try>` body. When an error occurs, execution of the `<try>` body immediately stops at the point of failure — any statements after the error are skipped — and control transfers to the `<catch>` block.

The `<catch>` block provides access to the error message through its `var` attribute. This variable receives a string containing the error message text, which you can use for logging, user-facing error responses, or conditional error handling logic. If the `var` attribute is omitted, the `<catch>` block still executes on error, but you lose access to the error message. This is acceptable when you know what the error will be and don't need the specific message, but in most cases you should capture it for logging or diagnostics.

Full reference →

### Child element: `else` — Success-path execution

Occurrence: "**single**"

The `<else>` block executes **only** when the `<try>` body completes successfully without any errors. This is the inverse of `<catch>` — exactly one of `<catch>` or `<else>` will execute for any given `<try>` invocation, never both.

The `<else>` block is useful for actions that should only happen on success, such as committing a transaction, sending a success response, or updating a status flag. Placing these actions in the `<else>` block rather than at the end of the `<try>` body ensures they never execute if an error occurred partway through the body.

### Child element: `finally` — Guaranteed execution

Occurrence: "**single**"

The `<finally>` block **always** executes after the `<try>` body and whichever of `<catch>` or `<else>` ran. It executes regardless of whether an error occurred, making it the ideal place for cleanup code that must run in all circumstances — sending HTTP headers, closing connections, outputting final response wrappers, or releasing resources.

Full reference →

### Execution flow

Understanding the exact order of execution is critical for writing correct error handling code. Here are the two possible flows:

**When no error occurs:**
1. The `<try>` body executes completely
2. The `<else>` block executes (if present)
3. The `<finally>` block executes (if present)

**When an error occurs:**
1. The `<try>` body executes until the error is thrown
2. Remaining statements in the `<try>` body are **skipped**
3. The `<catch>` block executes (if present), with the error message in `var`
4. The `<finally>` block executes (if present)

:::note
If `<catch>` is not present and an error occurs, the error propagates upward to the next enclosing `<try>` block, or terminates execution if there is none. Always include a `<catch>` block unless you intentionally want errors to propagate.
:::

:::tip
You can nest `<try>` blocks to handle errors at different scopes. An inner `<catch>` handles its own scope; if re-thrown with `<error>`, the outer `<catch>` handles it.
:::

### Examples

**Basic error handling:**

This example demonstrates the simplest form of try/catch — protecting code that might fail and capturing the error message:

```xml
<try>
    <set var="name">iXML</set>

    <if value1="$name" func="=" value2="iXML">
        <error>An error has occurred!</error>
    </if>

    <catch var="error">
        <output>$error</output>
    </catch>
</try>

<!-- An error has occurred! -->
```

The `<error>` statement throws an exception with the message "An error has occurred!". Execution immediately jumps to the `<catch>` block, which receives the message in the `$error` variable and outputs it. The `<if>` closing tag and any subsequent statements in the `<try>` body are skipped.

**Extended error handling with `else` and `finally`:**

This example shows the full four-part structure. The `<else>` block overwrites the initial status because no error occurs, and `<finally>` outputs the final value regardless:

```xml
<try>
    <set var="name">iXML</set>

    <catch>
        <set var="output">My name is unknown!</set>
    </catch>

    <else>
        <set var="output">My name is $name!</set>
    </else>

    <finally>
        <output>$output</output>
    </finally>
</try>

<!-- My name is iXML! -->
```

Since no error occurs in the `<try>` body, the `<catch>` block is skipped, the `<else>` block sets `output` to "My name is iXML!", and the `<finally>` block outputs the result.

**Defensive JSON parsing:**

One of the most common error handling patterns in ZeyOS services is protecting against malformed JSON input. The `<decode:json>` command throws an error when the input is not valid JSON, which you can catch and convert into a structured error response:

```xml
<array var="RES" />

<try>
    <decode:json var="payload">$BODY</decode:json>
    <set var="RES.result">1</set>
    <set var="RES.data" var_source="payload" />

    <catch var="err">
        <set var="RES.error">Invalid JSON input</set>
        <set var="RES.details">$err</set>
    </catch>
</try>

<header>Content-Type: application/json</header>
<output><encode:json var="RES" /></output>
```

If the JSON is valid, the response contains `{"result": 1, "data": {...}}`. If parsing fails, the response contains `{"error": "Invalid JSON input", "details": "Syntax error"}`. In both cases, the client receives a properly formatted JSON response with appropriate HTTP headers.

**Database error wrapping:**

When performing multi-step database operations, wrap the entire sequence in `<try>/<catch>` combined with `<db:transaction>` to ensure atomicity. If any statement fails, the transaction is automatically rolled back and the error is caught:

```xml
<try>
    <db:transaction>
        <db:insert table="contacts" var_result="contactId">
            <db:value field="lastname">$payload.lastname</db:value>
            <db:value field="firstname">$payload.firstname</db:value>
        </db:insert>

        <db:insert table="tasks">
            <db:value field="contactid">$contactId</db:value>
            <db:value field="name">Welcome task</db:value>
        </db:insert>
    </db:transaction>

    <catch var="err">
        <header status="500" />
        <set var="RES.error">Database operation failed</set>
        <set var="RES.details">$err</set>
    </catch>

    <else>
        <set var="RES.result">1</set>
        <set var="RES.contactId">$contactId</set>
    </else>

    <finally>
        <header>Content-Type: application/json</header>
        <output><encode:json var="RES" /></output>
    </finally>
</try>
```

The `<finally>` block ensures the JSON response is always sent, whether the database operations succeeded or failed. The `<else>` block only populates success data if no error occurred. The `<catch>` block sets the HTTP status to 500 and populates error details.

**External service error handling:**

When calling external REST or SOAP services, network failures, timeouts, and unexpected response formats can all throw errors. Wrap external calls in `<try>/<catch>` and provide fallback behavior:

```xml
<try>
    <rest:client url="https://api.example.com" timeout="10">
        <rest:bind var="getStatus" method="GET">/status</rest:bind>
    </rest:client>

    <call func="getStatus" var="statusData" />

    <catch var="err">
        <set var="statusData.available">0</set>
        <set var="statusData.error">$err</set>
    </catch>
</try>

<!-- statusData is now populated regardless of whether the external call succeeded -->
```

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/introduction/arithmetic

# Arithmetic Expressions

An arithmetic expression is a placeholder for its result. Whenever a number is explicitly expected (attributes of type `int` or `float`), the corresponding expression is automatically evaluated arithmetically. Its result is then implicitly cast to the appropriate input type.

This automatic evaluation means that you can write complex mathematical expressions directly in attribute values. For example, `<for var="i" from="2 * 3" to="(10 + 5) / 3">` evaluates `from` to `6` and `to` to `5` (truncated to integer because the `to` attribute has type `int`).

All **C-like arithmetic and bitwise operators** as well as their respective precedences are applicable to arithmetic expressions. This includes addition (`+`), subtraction (`-`), multiplication (`*`), division (`/`), modulo (`%`), and bitwise operations (`&`, `|`, `^`, `~`, `<<`, `>>`), as well as parentheses for grouping.

## Number notation

Integer numbers can be specified in multiple bases:

| Base | Prefix | Example | Decimal Equivalent |
|---|---|---|---|
| Decimal (base 10) | none | `123` | 123 |
| Hexadecimal (base 16) | `0x` | `0x1A` | 26 |
| Octal (base 8) | `0` | `017` | 15 |
| Binary (base 2) | `0b` | `0b1101` | 13 |

Floating point numbers can be specified in **decimal** (base 10) notation or **scientific E notation**. Scientific notation uses the format `<mantissa>e<exponent>`, where the result equals `mantissa x 10^exponent`. For example, `1.5e3` equals `1500.0`, and `2.5e-2` equals `0.025`.

:::warning
The octal prefix `0` can cause unexpected results. If you write `<for from="010">`, the runtime interprets `010` as octal 10, which is decimal `8`. Always use the explicit `0o` or bare decimal notation to avoid this ambiguity.
:::

## Expression evaluation in command attributes

Arithmetic expressions are evaluated automatically wherever an attribute of type `int` or `float` is expected:

```xml
<math:abs var="number">(0x12 - 19.5) * $number</math:abs>
```

In this example, `0x12` (hexadecimal 18) minus `19.5` equals `-1.5`, which is then multiplied by the value of `$number`. The [`math:abs`](https://www.zeyos.com/ixml/ref/math-abs) command takes the absolute value of the result.

## Inline arithmetic substitution

The `$()` syntax embeds arithmetic results directly into string context, even where an arithmetic type is not expected. This allows you to perform calculations inside `<output>` content, `<set>` values, and other string contexts:

```xml
<set var="birthyear">1950</set>
<output>This person is $(2012 - $birthyear) years old!</output>
<!-- This person is 62 years old! -->
```

Inline arithmetic may be nested within array access, enabling computed indices:

```xml
<array var="names">
    <item>Bill Gates</item>
    <item>Steve Jobs</item>
</array>

<output>$names[0] and $names[$(0 + 1)] are competitors!</output>
<!-- Bill Gates and Steve Jobs are competitors! -->
```

### See also

- [eval](https://www.zeyos.com/ixml/ref/eval) — evaluating expressions and storing the result
- [cast](https://www.zeyos.com/ixml/ref/cast) — converting string values to numeric types

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/introduction/quick-reference

# Quick Reference

Use this chapter as a day-to-day lookup while writing iXML. It covers all namespaces with brief syntax reminders and links to the full documentation.

## Variables and Expressions

| Task | Pattern |
|------|---------|
| Set variable | `<set var="name">value</set>` |
| Set with key | `<set var="name" key="k">value</set>` |
| Unset variable | `<unset var="name"/>` |
| Substitute variable | `$name` |
| Key chain access | `$name.key` or `$name[0]` |
| Arithmetic | `$(3 + 2)` |
| Inline function call | `$fn(arg)` |
| Cast type | `<cast var="name" type="int"/>` |
| Check type | `<typeof var="name" var_result="t"/>` |
| Check class | `<instanceof var="obj" class="MyClass" var_result="r"/>` |
| String length | `<length var="len">$str</length>` |
| Array/string size | `<size var="s">$arr</size>` |
| Assign by reference | `<assign var="a" var_source="b"/>` |
| Clone variable | `<clone var="copy" var_source="original"/>` |
| Swap variables | `<swap var="a" var_source="b"/>` |
| Evaluate expression | `<eval var="result">$expr</eval>` |
| Global variable | `<global var="name"/>` |
| Literal dollar sign | `$$` |
| No substitution | `<verbatim>$not_substituted</verbatim>` |

## String Operations

| Task | Pattern |
|------|---------|
| Lowercase | `<tolower var="result">$str</tolower>` |
| Uppercase | `<toupper var="result">$str</toupper>` |
| Trim | `<trim var="result">$str</trim>` |
| Substring | `<substr var="result" from="0" length="5">$str</substr>` |
| Find position | `<pos var="result" find="needle">$str</pos>` |
| Replace | `<replace var="result" find="old" replace="new">$str</replace>` |
| Regex replace | `<replace var="result" find="/pattern/" replace="new" type="regex">$str</replace>` |
| Split | `<split var="result" delimiter=",">$str</split>` |
| Pad | `<pad var="result" length="10" padding="0" type="left">$str</pad>` |
| Concat | `<concat var="result" glue=", " var_result="out"/>` |
| Match regex | `<match var="result" pattern="/regex/">$str</match>` |
| Char code to char | `<char var="result">65</char>` |
| Char to code | `<ord var="result">A</ord>` |

## Control Flow

| Task | Pattern |
|------|---------|
| If equals | `<if value1="$a" value2="$b">...</if>` |
| If with operator | `<if value1="$a" func="&gt;" value2="$b">...</if>` |
| If regex | `<if value1="$a" func="~" value2="/pattern/">...</if>` |
| If/else | `<if ...>...<else>...</else></if>` |
| Type check | `<is var="x" type="array">...</is>` |
| Switch | `<switch value="$x"><case value="1">...</case><default>...</default></switch>` |
| For loop | `<for var="i" from="1" to="10">...</for>` |
| Foreach | `<foreach var="arr" var_key="k" var_value="v">...</foreach>` |
| While loop | `<while value1="$i" func="&lt;" value2="10">...</while>` |
| Break loop | `<break/>` |
| Next iteration | `<next/>` |
| Exit execution | `<exit/>` |
| Return value | `<return>value</return>` |

## Comparison Operators (func attribute)

| Operator | Description |
|----------|-------------|
| `=` | Equals |
| `!=` | Not equals |
| `&lt;` | Less than |
| `&lt;=` | Less than or equal |
| `&gt;` | Greater than |
| `&gt;=` | Greater than or equal |
| `=*` | Case-insensitive equals |
| `!=*` | Case-insensitive not equals |
| `_` | Contains |
| `!_` | Does not contain |
| `^` | Starts with |
| `!^` | Does not start with |
| `$` | Ends with |
| `!$` | Does not end with |
| `~` | Regex match |
| `!~` | Regex does not match |
| `~~` | LIKE pattern |
| `!~~` | NOT LIKE pattern |

## Functions, Classes, and Modules

| Task | Pattern |
|------|---------|
| Define function | `<function var="fn">...</function>` |
| Call function | `<call func="fn" var="result"/>` |
| Define macro | `<macro var="m">...</macro>` |
| Expand macro | `<expand macro="m"/>` |
| Define class | `<class var="C"><property>...<method>...</class>` |
| Instantiate | `<new class="C" var="obj"/>` |
| Include resource | `<include id="resource_id" type="resource"/>` |
| Pass parameter | `<param var="name"/>` |

## Error Handling

| Task | Pattern |
|------|---------|
| Try/catch | `<try>...<catch var="err">...</catch></try>` |
| Try/else (success) | `<try>...<else>...</else></try>` |
| Try/finally | `<try>...<finally>...</finally></try>` |
| Raise error | `<error>message</error>` |
| Raise warning | `<warning>message</warning>` |

## Array Operations

| Task | Pattern |
|------|---------|
| Create array | `<array var="arr"><item key="k">v</item></array>` |
| Push item | `<array:push var="arr">value</array:push>` |
| Pop item | `<array:pop var="arr" var_result="val"/>` |
| Shift item | `<array:shift var="arr" var_result="val"/>` |
| Unshift item | `<array:unshift var="arr">value</array:unshift>` |
| Sort | `<array:sort var="arr"/>` |
| Reverse | `<array:reverse var="arr"/>` |
| Merge | `<array:merge var="a" var_source="b"/>` |
| Slice | `<array:slice var="arr" var_result="sub" offset="0" length="5"/>` |
| Splice | `<array:splice var="arr" offset="2" length="1"/>` |
| Search | `<array:search var="arr" var_result="key">value</array:search>` |
| Key exists | `<array:keyexists var="arr" var_result="r" key="k"/>` |
| Keys | `<array:keys var="arr" var_result="keys"/>` |
| Values | `<array:values var="arr" var_result="vals"/>` |
| Join | `<array:join var="arr" var_result="str" glue=","/>` |
| Unique | `<array:unique var="arr"/>` |
| Filter | `<array:filter var="arr"/>` |
| Map | `<array:map var="arr" func="fn"/>` |
| Walk | `<array:walk var="arr" func="fn"/>` |
| Chunk | `<array:chunk var="arr" var_result="chunks" size="3"/>` |
| Column | `<array:column var="arr" var_result="col" key="name"/>` |
| Combine | `<array:combine var_keys="k" var_values="v" var_result="arr"/>` |

## Database

| Task | Pattern |
|------|---------|
| Get entity | `<db:get var="obj" entity="contacts" id="$id"/>` |
| Set entity | `<db:set entity="contacts" id="$id" var_data="obj"/>` |
| Remove entity | `<db:remove entity="contacts" id="$id"/>` |
| Lookup entity | `<db:lookup var="id" entity="contacts" field="email">$email</db:lookup>` |
| Select rows | `<db:select var="rows" entity="contacts" type="assoc" limit="10">...</db:select>` |
| Insert row | `<db:insert entity="contacts"><db:value field="name">Ada</db:value></db:insert>` |
| Update rows | `<db:update entity="contacts"><db:value field="name">New</db:value><db:is ...>...</db:update>` |
| Delete rows | `<db:delete entity="contacts"><db:is ...>...</db:delete>` |
| Raw SQL | `<db:run var="result" type="assoc">SELECT ...</db:run>` |
| Transaction | `<db:transaction>...</db:transaction>` |
| NULL check | `<db:isnull field="c.email"/>` |
| NOT NULL check | `<db:isnotnull field="c.email"/>` |
| IN list | `<db:in var="list" field="c.type"/>` |
| Full-text search | `<db:search query="term"><db:searchfield field="c.name"/></db:search>` |

**Result types:** `num` (default), `assoc`, `list`, `av`, `single`, `self`, `value`

## REST and HTTP

| Task | Pattern |
|------|---------|
| REST server | `<rest:server>...</rest:server>` |
| REST resource | `<rest:resource route="/items/:id" method="GET" var_body="body">...</rest:resource>` |
| HTTP request | `<http:request var="resp" url="..." method="GET"/>` |
| URL parse | `<http:url var="parts">$url</http:url>` |
| Query string | `<http:query var="params"/>` |
| Set HTTP status | `<set var="return">404</set>` |
| Set header | `<header>Content-Type: application/json</header>` |

## Encoding / Decoding

| Format | Encode | Decode |
|--------|--------|--------|
| JSON | `<encode:json var="x" var_result="raw" pretty="1"/>` | `<decode:json var="x">$raw</decode:json>` |
| XML entities | `<encode:xml var="safe">$text</encode:xml>` | `<decode:xml var="plain">$safe</decode:xml>` |
| CSV | `<encode:csv var="rows" var_result="csv" delimiter=";"/>` | `<decode:csv var="rows" delimiter=";">$csv</decode:csv>` |
| Base64 | `<encode:base64 var="enc">$data</encode:base64>` | `<decode:base64 var="dec">$enc</decode:base64>` |
| URL | `<encode:url var="enc">$val</encode:url>` | `<decode:url var="dec">$enc</decode:url>` |
| Hash | `<encode:hash var="h" algorithm="sha256">$data</encode:hash>` | — |
| HMAC | `<encode:hash var="h" algorithm="sha256" key="secret">$data</encode:hash>` | — |
| AES encrypt | `<encode:aes var="enc" key="..." iv="...">$data</encode:aes>` | `<decode:aes var="dec" key="..." iv="...">$enc</decode:aes>` |
| Compress | `<encode:deflate var="compressed">$data</encode:deflate>` | `<decode:inflate var="decompressed">$data</decode:inflate>` |

## PDF

| Task | Pattern |
|------|---------|
| Create document | `<pdf:document var="pdf" title="...">...</pdf:document>` |
| Add section | `<pdf:section format="A4" orientation="P">...</pdf:section>` |
| Header/footer/body | `<pdf:header>...</pdf:header>` |
| Style text | `<pdf:style fontsize="10" fontstyle="B">...</pdf:style>` |
| Block text | `<pdf:block>text</pdf:block>` |
| Inline text | `<pdf:inline>text</pdf:inline>` |
| Table row | `<pdf:row><pdf:col width="50">text</pdf:col></pdf:row>` |
| Image | `<pdf:image filename="path" width="40"/>` |
| Barcode | `<pdf:barcode type="QRCODE,M" width="30">data</pdf:barcode>` |
| Line break | `<pdf:linebreak offset="5"/>` |
| Page break | `<pdf:pagebreak/>` |
| Get position | `<pdf:getpos var_page="p" var_y="y"/>` |
| Set position | `<pdf:setpos y="100"/>` |

## Excel

| Task | Pattern |
|------|---------|
| Create workbook | `<excel:workbook>...</excel:workbook>` |
| Set cell | `<excel:set coords="A1" type="numeric">42</excel:set>` |
| Get cell | `<excel:get var="val" coords="A1"/>` |
| Move cursor | `<excel:setpos sheet="0" row="5" col="0"/>` |
| Add sheet | `<excel:addsheet>Title</excel:addsheet>` |
| Apply style | `<excel:style var="styleArray">A1:B5</excel:style>` |
| Auto-filter | `<excel:autofilter>A1:D100</excel:autofilter>` |
| Export to array | `<excel:array var="data"/>` |
| Generate file | `<excel:create var="book" format="xlsx"/>` |

## File Operations

| Task | Pattern |
|------|---------|
| Read file | `<file:read var="content" filename="path"/>` |
| Write file | `<file:write filename="path">content</file:write>` |
| Append | `<file:append filename="path">content</file:append>` |
| Read CSV | `<file:readcsv var="rows" filename="path" delimiter=","/>` |
| Read JSON | `<file:readjson var="data" filename="path"/>` |
| Write CSV | `<file:writecsv var="rows" filename="path" delimiter=","/>` |
| Write JSON | `<file:writejson var="data" filename="path"/>` |
| File exists | `<file:exists var="e" filename="path"/>` |
| File type | `<file:typeof var="t" filename="path"/>` |
| Path info | `<file:pathinfo var="info" filename="path"/>` |
| List directory | `<file:listdir var="files" dirname="path"/>` |
| Glob | `<file:glob var="files" pattern="*.csv"/>` |
| Delete | `<file:delete filename="path"/>` |
| Make directory | `<file:makedir dirname="path"/>` |

## ZIP

| Task | Pattern |
|------|---------|
| Open archive | `<zip:archive filename="path">...</zip:archive>` |
| Write entry | `<zip:write filename="entry">content</zip:write>` |
| Read entry | `<zip:read var="data" filename="entry"/>` |
| List entries | `<zip:list var="entries"/>` |
| Check entry | `<zip:exists var="found" filename="entry"/>` |
| Delete entry | `<zip:delete filename="entry"/>` |

## ZeyOS Platform

| Task | Pattern |
|------|---------|
| Get custom fields | `<extdata:get var="data" entity="contacts" id="$id"/>` |
| Set custom fields | `<extdata:set entity="contacts" id="$id" var_data="data"/>` |
| List tags | `<tags:list var="tags" entity="contacts" id="$id"/>` |
| Add tags | `<tags:add entity="contacts" id="$id"><tags:name>VIP</tags:name></tags:add>` |
| Store temp data | `<temp:write var="id">content</temp:write>` |
| Read temp data | `<temp:read var="content" id="$id"/>` |
| Get temp path | `<temp:path var="path" id="$id"/>` |
| Store binary | `<bin:write var="id">data</bin:write>` |
| Next number | `<numformat:next var="num" name="sequence"/>` |
| Authenticate | `<auth:simple var="user" username="..." password="..."/>` |
| Get session | `<session var="session"/>` |
| Get config | `<config var="config"/>` |
| Run Zymba | `<zymba:run var="result"><zymba:code>...</zymba:code></zymba:run>` |

## Common Pitfalls

| Pitfall | Correct Approach |
|---------|-----------------|
| `<if condition="...">` | Use `value1`, `value2`, `func` attributes |
| `<set var="$name">` | Remove `$` — it is substitution syntax only |
| `func="<"` in attribute | Escape as `func="&lt;"` |
| `<set var="x">42</set>` is integer | It's a string — use `<cast var="x" type="int"/>` |
| `<db:is field="x">NULL</db:is>` | Use `<db:isnull field="x"/>` |
| `<db:isnot ...>` | Does not exist — use `<db:is func="!=">` |
| `<encode:hmac>` | Does not exist — use `<encode:hash key="secret">` |
| `<array:count>` | Does not exist — use `<size var="n">$arr</size>` |
| `<decode:json>` without `var` | Always use `var` to capture the parsed result |
| CSV delimiter assumed comma | Default is `;` — specify `delimiter=","` explicitly |

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/introduction/result-propagation

# Result Propagation

Any CDATA value of a statement may be replaced with the result of an embedded statement, unless it is explicitly precluded (by the `no-result-propagation` predicate). An incompatible result type is thereby implicitly cast to the appropriate input type.

Result propagation is one of iXML's most distinctive features. It allows you to compose statements by nesting them, with the inner statement's result flowing upward to become the content of the outer statement. This eliminates the need for temporary variables in many common patterns and enables a concise, functional style of coding.

## Basic propagation

Instead of storing a value in a variable and then referencing it, you can embed the producing statement directly inside the consuming statement:

```xml
<!-- Two-step approach with a temporary variable: -->
<set var="number">$(15 + (7.5 * 2 - 3))</set>
<output>$number</output>
<!-- 27 -->

<!-- Single-step approach with result propagation: -->
<output>
    <set>$(15 + (7.5 * 2 - 3))</set>
</output>
<!-- 27 -->
```

In the second form, the `<set>` statement (without a `var` attribute) computes the value `27` and propagates it as the content of the `<output>` statement.

## Simultaneous storage and propagation

The result may still be stored in a result variable if specified, while also propagating upward:

```xml
<output>
    <set var="number">$(15 + (7.5 * 2 - 3))</set>
</output>

<output>,$number</output>

<!-- 27,27 -->
```

The first `<output>` receives the propagated result `27` and outputs it. Simultaneously, the value is stored in `$number`, which the second `<output>` references.

## Last scalar result wins

When multiple embedded statements produce results within the same parent, the **last scalar result** is used for propagation:

```xml
<output>
    <set var="number">10</set>
    <set>$($number + 100)</set>
    <set>$($number + 5)</set>
</output>
<!-- 15 -->
```

The first `<set>` stores `10` in `number` and propagates `"10"`. The second produces `110`. The third produces `15`. Since the **last** scalar result wins, the `<output>` receives `15`. The intermediate results are discarded from the propagation chain (though the side effect of storing `10` in `$number` persists).

## Deep nesting with control flow

The embedded code may also be deeply nested. Results of `IF`, `IS`, and `SWITCH` are propagated to the topmost statement, enabling inline conditional output:

```xml
<set var="born">1945</set>

<output>
    <if value1="$born" func="&lt;" value2="1950">
        <set>This person is very old!</set>

        <else>
            <set>$(2012 - $born)</set>
        </else>
    </if>
</output>

<!-- This person is very old! -->
```

## CDATA alongside embedded code is discarded

Any CDATA values (plain text) that are used alongside embedded code are discarded. Once you embed any statement inside a parent element, all plain text content in that parent is ignored:

```xml
<output>
    <set>$(15 + (7.5 * 2 - 3))</set>
    471
</output>
<!-- 27 -->
```

The literal text `471` is discarded because embedded code is present. Only the result of the `<set>` statement (`27`) is propagated.

## Propagation with function calls

Result propagation is also applicable to user-defined functions. The `<call>` command propagates its return value to the parent statement:

```xml
<function var="getName">
    <set var="return">iXML</set>
</function>

<output>
    <call func="getName"/>
</output>

<!-- iXML -->
```

### See also

- [set](https://www.zeyos.com/ixml/ref/set) — the most common statement used in propagation patterns
- [call](https://www.zeyos.com/ixml/ref/call) — function calls with result propagation
- [Syntax Essentials](https://www.zeyos.com/ixml/ixml-guide/syntax-essentials) — how `if`, `is`, and `switch` propagate results

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/introduction/scope-and-assignment

# Scope and Assignment

## Scope and visibility

Variables in iXML exist within a **scope** — a boundary that determines where the variable can be referenced. Understanding scope is essential for writing correct iXML code, especially when working with functions, includes, and closures.

Variables typically have a **single local scope** and are therefore only visible within the immediate context, unless they have been specifically declared as global variables, in which case they have **global scope** and are independent of the context within which they are defined.

**Closures** may be used to establish a referencing environment for free function variables. Those variables are explicitly bound to corresponding variables from the immediate context within which a function is defined, thereby directly referencing non-local variables from the parent scope outside of the inherent context of that function.

### Visibility precedence

When multiple variables with the same name exist at different scope levels, iXML resolves the reference using a strict precedence order from highest to lowest priority:

1. **Special local variables** — `this`, `return`, `arguments`. These are automatically provided by the runtime inside functions and cannot be overridden by any other mechanism.
2. **Free function variables** — declared with `<use>` in closures. These are bound references to variables in the function's defining scope.
3. **Regular local variables** — parameters passed on context switch (via `<param>` in `<call>`, `<include>`, or `<new>`). These are the most common variables in everyday code.
4. **Global variables** — declared with `<global>`. These have the lowest precedence, meaning any local variable with the same name shadows the global.

By default, variables are local to context boundaries (function calls, includes, REST resources). Use `<global var="..." />` when cross-context sharing is intentional, but prefer parameter passing for cleaner, more testable code.

### See also

- [function](https://www.zeyos.com/ixml/ref/function) — context isolation and closure binding
- [global](https://www.zeyos.com/ixml/ref/global) — declaring global variables
- [include](https://www.zeyos.com/ixml/ref/include) — context boundaries in code inclusion

---

## Assignment model

The way iXML assigns values to variables depends on the type of value being assigned. This distinction has profound implications for how your code behaves, particularly when passing complex data between functions, modifying arrays during iteration, and building object-oriented structures.

- **Primitive non-composite variables** (null, bool, int, float, string) are always assigned **by value**. When you assign a primitive to another variable, the runtime creates an independent copy. Changes to one variable do not affect the other.
- **Complex variables** (array, function, macro, class) are always assigned **by reference**. When you assign a complex value to another variable, both variables point to the same underlying data. Changes through one variable are visible through the other.
- Assignment by reference may cause **circular references**. Avoid creating data structures where array A contains a reference to array B, which in turn contains a reference back to array A — this can cause stack depth exceptions.
- Using a variable before assigning a value to it is possible — it then **defaults to the neutral literal** of the accordant input type. For example, an uninitialized variable used in an arithmetic context defaults to `0`.

:::warning
The by-reference behavior of arrays is one of the most important concepts to internalize. When you pass an array to a function via `<param>`, the function receives a reference to the original array — modifications inside the function affect the caller's data. Use `<clone>` if you need an independent copy.
:::

### See also

- [assign](https://www.zeyos.com/ixml/ref/assign) — explicit by-reference assignment
- [clone](https://www.zeyos.com/ixml/ref/clone) — creating independent copies of complex variables
- [typeof](https://www.zeyos.com/ixml/ref/typeof) — inspecting the runtime type of a variable
- [cast](https://www.zeyos.com/ixml/ref/cast) — explicitly converting between types

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/introduction/variables-and-substitution

# Variables and Substitution

The name of a variable is a placeholder for its value — the data it holds. Referencing that value is called **variable substitution**. Variables referenced inside an expression are expanded to their respective values. For this purpose, variables are represented by a dollar sign (`$`) followed by the name of the variable.

Variable substitution is the mechanism that makes iXML dynamic. Without substitution, every `<output>` statement would produce static text. With substitution, you can embed computed values, database results, API responses, and user input directly into your output, your conditionals, and your data structures.

## Basic substitution

The simplest form of variable substitution uses the dollar sign followed by a variable name. The variable name must consist of alphanumeric characters and underscores only, and it is **case-sensitive**:

```xml
<set var="name">iXML</set>
<output>My name is $name!</output>
<!-- My name is iXML! -->
```

When the runtime encounters `$name` in the output string, it looks up the variable `name` in the current scope, finds the value `"iXML"`, and replaces `$name` with that value. The exclamation mark after `$name` is not part of the variable name because it is a punctuation character — the runtime treats any punctuation character as the end of a variable name.

## Substitution rules

- **Variable names are case-sensitive** and consist of alphanumeric characters and underscores only. The variable `$Name` and the variable `$name` are two distinct variables.
- The next **punctuation character** implicitly ends the variable name, though it may explicitly be concluded by a trailing dollar sign (`$`). For example, in the string `$name!`, the `!` terminates the variable name. If you need to concatenate a variable with alphanumeric text, use the trailing dollar sign: `$name$suffix` reads two variables, but `$name$$suffix` reads `$name`, outputs a literal `$`, then reads `suffix`.
- A dollar sign (`$`) is passed unchanged if **not followed by** an alphanumeric character or underscore. For example, `$!` outputs a literal `$!` because `!` is not a valid variable name character.
- Two adjacent dollar signs (`$$`) are substituted with one single dollar sign (`$`) as an **escape syntax**. This is the only way to produce a literal dollar sign in output.

### Dollar sign escaping

When you need to output a literal dollar sign — for example, in currency formatting — use the double-dollar escape:

```xml
<output>Price: $$100</output>
<!-- Price: $100 -->
```

### Invalid variable declaration

The `$` symbol is for substitution only — it is not part of variable names. Including a dollar sign in the `var` attribute of any command is a parser error:

```xml
<set var="$a">hello</set>
<!-- Parser error: Invalid variable name for attribute "VAR" -->
```

The correct form is `<set var="a">hello</set>`. When you later reference this variable, you write `$a` — the dollar sign belongs to the *substitution expression*, not to the *variable name*.

---

## Array key access and key chains

iXML provides a shortcut syntax for referencing array items directly by specifying the corresponding key. This syntax supports two notations — **array notation** (square brackets) and **object notation** (dot syntax) — which can be mixed freely and nested to arbitrary depth.

### Array notation (square brackets)

Within square brackets, a key is **not interpreted as a literal** but as an **expression**, resulting in further variable substitution. This means you can use variables as dynamic keys, enabling patterns like lookup tables and computed array access.

Empty brackets `[]` have context-dependent behavior:
- In **substitution** (`$arr[]`) or **getting/unsetting**: interpreted as an empty key literal
- In **setting** (`<set var="arr[]">value</set>`): implicitly generates the next consecutive numeric key, similar to pushing onto an array

```xml
<set var="names[bg]">Bill Gates</set>
<set var="names[sj]">Steve Jobs</set>
<output>$names[bg] and $names[sj] are competitors!</output>
<!-- Bill Gates and Steve Jobs are competitors! -->
```

Dynamic key lookup with nested substitution — the keys themselves are resolved from variables:

```xml
<set var="names[bg]">Bill Gates</set>
<set var="names[sj]">Steve Jobs</set>
<set var="key1">bg</set>
<set var="key2">sj</set>
<output>$names[$key1] and $names[$key2] are competitors!</output>
<!-- Bill Gates and Steve Jobs are competitors! -->
```

### Object notation (dot syntax)

A key appended with a dot is **always interpreted as a literal**. No variable substitution occurs within dot-notation keys. This makes dot notation ideal for accessing known, fixed keys — it is more readable and slightly faster than bracket notation for this purpose.

The restriction is that dot-notation keys may consist of **alphanumeric characters and underscores only**. If you need to access a key containing special characters (spaces, hyphens, dots), you must use bracket notation.

```xml
<set var="names.bg">Bill Gates</set>
<set var="names.sj">Steve Jobs</set>
<output>$names.bg and $names.sj are competitors!</output>
<!-- Bill Gates and Steve Jobs are competitors! -->
```

### Key chains

Several keys may be strung together to form a **key chain** of arbitrary length, which allows multidimensional arrays to be accessed directly. Array notation and dot notation may be mixed freely within a single key chain:

```xml
<set var="names.microsoft[bg]">Bill Gates</set>
<set var="names[apple].sj">Steve Jobs</set>
<output>$names[microsoft].bg and $names.apple[sj] are competitors!</output>
<!-- Bill Gates and Steve Jobs are competitors! -->
```

### Deeply nested references

References of array items may be deeply nested within the array notation. This enables indirection patterns where one array acts as a lookup table for keys into another array:

```xml
<set var="names.bg">Bill Gates</set>
<set var="names[sj]">Steve Jobs</set>
<set var="keys[bill_gates]">bg</set>
<set var="keys.steve_jobs">sj</set>
<output>$names[$keys.bill_gates] and $names[$keys[steve_jobs]] are competitors!</output>
<!-- Bill Gates and Steve Jobs are competitors! -->
```

Nested references can be stacked to any depth, but deeply nested expressions become difficult to read and debug — consider using intermediate variables for clarity in production code.

---

## Inline expression forms

iXML provides two special substitution syntaxes for embedding computed values directly into strings: **arithmetic inline substitution** for mathematical expressions, and **inline function substitution** for calling functions with a single argument.

### Arithmetic inline substitution

Arithmetic expressions embedded inside another expression are replaced by their result. They are represented by a leading dollar sign (`$`) followed by parentheses (`()`):

```xml
<set var="birthyear">1950</set>
<output>This person is $(2012 - $birthyear) years old!</output>
<!-- This person is 62 years old! -->
```

The `$()` syntax works anywhere that normal substitution works — in `<output>` content, in `<set>` content, in attribute values of type `string`, and even inside other substitution expressions.

Computed array indices:

```xml
<array var="names">
    <item>Bill Gates</item>
    <item>Steve Jobs</item>
</array>

<output>$names[0] and $names[$(0 + 1)] are competitors!</output>
<!-- Bill Gates and Steve Jobs are competitors! -->
```

### Inline function substitution

A leading variable reference followed by a single argument in parentheses calls the function and substitutes its return value:

```xml
<function var="lcase">
    <tolower var="return">$return</tolower>
</function>

<set var="name">iXML</set>
<output>My name is $lcase($name)!</output>
<!-- My name is ixml! -->
```

The runtime calls the function with `$name` as the value of the special `return` variable, executes the function body, and inserts the return value into the output string.

:::note
The inline function syntax `$func($arg)` passes the argument as the `return` variable. This is a shorthand — for multiple arguments, use the [`call`](https://www.zeyos.com/ixml/ixml-guide/reusability-and-oop/functions) command with `<param>` children instead.
:::

### See also

- [set](https://www.zeyos.com/ixml/ref/set) — the primary variable assignment command
- [call](https://www.zeyos.com/ixml/ref/call) — invoking functions with multiple parameters
- [output](https://www.zeyos.com/ixml/ref/output) — producing output with embedded substitution

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/reusability-and-oop

# Reusability and Object-Orientation

Large iXML codebases become maintainable only when reuse boundaries are explicit. iXML provides five primary mechanisms for structuring reusable code:

- **[Functions](https://www.zeyos.com/ixml/ixml-guide/reusability-and-oop/functions)**: Closed subroutines with local scope.
- **[Macros](https://www.zeyos.com/ixml/ixml-guide/reusability-and-oop/macros)**: Open subroutines that share the caller's context.
- **[Classes](https://www.zeyos.com/ixml/ixml-guide/reusability-and-oop/classes)**: Array prototypes for object-oriented design.
- **[Includes](https://www.zeyos.com/ixml/ixml-guide/reusability-and-oop/functions#include--include-code-source)**: External code source execution.
- **[Client Bindings](https://www.zeyos.com/ixml/ixml-guide/reusability-and-oop/clients)**: SOAP and REST service integration.

Each mechanism has different scoping, performance, and composability characteristics. Choosing the right one is critical for building robust ZeyOS applications.

## Decision Framework

| Need | Recommended construct | Why |
|---|---|---|
| Small, context-aware template fragment | `macro` + `expand` | No context overhead; direct access to surrounding variables makes it natural for output patterns |
| Reusable logic with clear input/output contract | `function` + `call` | Context isolation prevents accidental side effects; parameters and return value create an explicit API |
| State and behavior that belong together | `class` + `new` | Properties hold state; methods provide behavior; inheritance enables hierarchical reuse |
| Code in a separate file or ZeyOS resource | `include` | Natural module boundary; local context provides isolation between files |
| External SOAP/REST web service | Client Bindings | Automatic WSDL/Route parsing; operations become callable functions |

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/reusability-and-oop/classes

# Classes and Objects

A **class** in iXML is an array prototype. It defines properties, a constructor, and methods. An **object** is an instance of a class, created with `<new>`.

## `class` — Class definition

[Full reference →](https://www.zeyos.com/ixml/ref/class)

### Syntax

```xml
<class var="MyClass">
    <extends class="ParentClass"/>

    <property name="propName">default value</property>

    <constructor>
        <set var="this.propName">$paramValue</set>
    </constructor>

    <method name="methodName">
        <set var="return">$this.propName</set>
    </method>
</class>
```

### Constituents

- **Properties**: Default key-value pairs.
- **Constructor**: Automatic initialization routine.
- **Methods**: Functions bound to the instance check.

## `new` — New instance

[Full reference →](https://www.zeyos.com/ixml/ref/new)

Creates a new object from a class.

### Syntax

```xml
<new class="ClassName" var="objectVar">
    <param name="paramName">value</param>
</new>
```

## `method` — Method definition

Full reference →

Defines a function bound to the class. Inside a method, `$this` refers to the object instance.

## Inheritance

Classes can inherit from parent classes using `<extends>`.
- The child inherits all properties and methods.
- Constructors execute in chain order: Parent first, then Child.
- Methods can be overridden by redefining them in the child.

## Introspection

- **`instanceof`**: Check if an object is an instance of a class.
- **`classinfo`**: Retrieve structural information about a class.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/reusability-and-oop/clients

# Client Bindings

## `soap:client` — SOAP Client

[Full reference →](https://www.zeyos.com/ixml/ref/soap-client)

Defines SOAP operations from a WSDL.

### Syntax

```xml
<soap:client wsdl="https://example.com/service?wsdl">
    <soap:bind var="myFunc">OperationName</soap:bind>
</soap:client>

<call func="myFunc" var="result" />
```

## `rest:client` — REST Client

[Full reference →](https://www.zeyos.com/ixml/ref/rest-client)

Defines RESTful resources.

### Syntax

```xml
<rest:client url="https://api.example.com">
    <rest:header>Authorization: Bearer $token</rest:header>
    <rest:bind var="getItem" method="GET">/items/:id</rest:bind>
</rest:client>

<call func="getItem" var="item">
    <param var="id">123</param>
</call>
```

### Route Parameters

parameters passed to `<call>` fill the `:name` placeholders in the route path.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/reusability-and-oop/functions

# Functions and Includes

## `function` — Function definition

[Full reference →](https://www.zeyos.com/ixml/ref/function)

A function in iXML is a **closed subroutine** — a named, reusable block of code that executes in its own isolated local context.

### Syntax

```xml
<function var="myFunction">
    <use name="closureVarName" var="outerVar"/>

    <!-- function body -->
    <set var="return">result value</set>
</function>
```

### Context Isolation

The critical characteristic of a function is **context isolation**. When a function is called, iXML creates a fresh local context. Variables declared inside are invisible outside, and vice versa, unless explicitly bound.

### Special Local Variables

| Variable | Description |
|---|---|
| `this` | Reference to the calling object (if called as a method). |
| `return` | The return value. Defaults to `NULL`. |
| `arguments` | Array containing all passed parameters. |

## `call` — Call function

[Full reference →](https://www.zeyos.com/ixml/ref/call)

Invokes a function. Execution transfers to the function and returns when complete.

### Syntax

```xml
<call func="functionName" var="resultVar">
    <param name="paramName">value</param>
</call>
```

### Attributes

| Name | Type | Description |
|---|---|---|
| `func` | var | Function variable to call. |
| `var` | var | Variable to store the return value. |
| `var_params` | var | Array variable containing parameters. |
| `var_this` | var | Explicitly bind `this` context. |

## `include` — Include code source

[Full reference →](https://www.zeyos.com/ixml/ref/include)

Executes code from an external resource file.

### Syntax

```xml
<include id="resource.id" [var="classVar"] [once="true"]>
    <param name="paramName">value</param>
</include>
```

### Description

`include` loads and executes the content of a ZeyOS resource. It is the primary way to modularize code into separate files.

- By default, includes execute in a **new local context** (like a function call).
- Parameter passing works exactly like `<call>`.
- Use `once="true"` to prevent re-loading the same library multiple times.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/reusability-and-oop/macros

# Macros

## `macro` — Macro definition

[Full reference →](https://www.zeyos.com/ixml/ref/macro)

A macro is an **open subroutine**. Unlike a function, it executes within the **current context** of the caller. It has direct access to all variables in the calling scope.

### Syntax

```xml
<macro var="myMacro">
    <!-- macro body — executes in caller's context -->
    <output>Hello $name</output>
</macro>
```

### When to use

Macros are ideal for:
- Template fragments
- Repetitive output patterns
- Simple utility routines where context overhead is unwanted

**Warning**: Macros can accidentally overwrite variables in the caller's scope.

## `expand` — Expand macro

[Full reference →](https://www.zeyos.com/ixml/ref/expand)

Invokes a macro.

### Syntax

```xml
<expand macro="macroName"/>
```

### Example

```xml
<set var="name">World</set>
<macro var="greet"><output>Hello $name</output></macro>
<expand macro="greet"/>
<!-- Hello World -->
```

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/reusability-and-oop/patterns

# Object-Oriented Patterns

This section covers practical patterns for building maintainable applications.

## The `this` Reference

`this` is a direct reference to the current object.
- **Access**: `$this.property`
- **Method Call**: `$this.method()`

## Factory Pattern

Use a factory class to decide which class to instantiate at runtime (e.g., selecting a print template based on data).

```xml
<class var="ShapeFactory">
    <method name="create">
        <if value1="$type" value2="circle">
             <new class="Circle" var="return" />
        <else>
             <new class="Square" var="return" />
        </else>
        </if>
    </method>
</class>
```

## Singleton Pattern

Combine `<global>` with a conditional check to ensure only one instance exists.

```xml
<global var="Logger" />
<is var="Logger" type="non-array">
    <new class="LoggerClass" var="Logger" />
</is>
```

## Composition

Prefer composition over deep inheritance. Objects can contain other objects as properties.

```xml
<class var="Invoice">
    <constructor>
        <new class="Customer" var="this.customer" />
        <new class="LineItems" var="this.items" />
    </constructor>
</class>
```

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/standard-library

# Standard Library

iXML comes with a rich standard library for common application tasks.

## Content Generation

- **[PDF Generation](https://www.zeyos.com/ixml/ixml-guide/standard-library/pdf)**: Create professional PDF documents using HTML/CSS or low-level primitives.
- **[Excel & ZIP](https://www.zeyos.com/ixml/ixml-guide/standard-library/excel-zip)**: Generate and read Excel spreadsheets and ZIP archives.

## Data Processing

- **[Math & DateTime](https://www.zeyos.com/ixml/ixml-guide/standard-library/math-datetime)**: Mathematical functions and date/time manipulation.
- **[Encoding & Decoding](https://www.zeyos.com/ixml/ixml-guide/standard-library/encoding)**: JSON, XML, Base64, and cryptographic utilities.

## System Interaction

- **[Filesystem](https://www.zeyos.com/ixml/ixml-guide/standard-library/filesystem)**: Read, write, and manage files in the ZeyOS VFS.
- **[HTTP & REST](https://www.zeyos.com/ixml/ixml-guide/standard-library/http-rest)**: Build RESTful services and handle HTTP requests.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/standard-library/encoding

# Encoding and Decoding

iXML provides encode/decode operations for cryptographic hashing, phonetic encoding, compression, encryption, character escaping, transfer encoding, and structured data formats. Most encode operations have a complementary decode counterpart.

## Cryptographic Hashing

### encode:hash

Generates a cryptographic hash using a specified algorithm. Supports HMAC when a `key` is provided.

```xml
<encode:hash var="signature" algorithm="sha256" key="$api_key">
    $request_data
</encode:hash>
```

:::warning
Use SHA-256 or higher for security-sensitive operations. Never use plain MD5/SHA-1 for password storage.
:::

[Full reference →](https://www.zeyos.com/ixml/ref/encode-hash)

### encode:md5 / encode:sha1

Convenience commands for MD5 (32-char hex) and SHA-1 (40-char hex) hashing. Equivalent to `encode:hash` with the respective algorithm.

```xml
<output><encode:md5>My name is iXML!</encode:md5></output>
<!-- 1eee4eaee83273f381b9158ac42c6ba5 -->
```

[encode:md5 reference →](https://www.zeyos.com/ixml/ref/encode-md5) | [encode:sha1 reference →](https://www.zeyos.com/ixml/ref/encode-sha1)

### encode:crc32

Generates a 32-bit cyclic redundancy checksum for error detection (not security). Returns a signed integer.

```xml
<output><encode:crc32>My name is iXML!</encode:crc32></output>
<!-- 812420598 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/encode-crc32)

## Phonetic Encoding

### encode:soundex / encode:metaphone

Generate phonetic keys for fuzzy name matching. Soundex produces a 4-character code; Metaphone uses English pronunciation rules for higher accuracy.

```xml
<output><encode:soundex>Smith</encode:soundex></output>
<!-- S530 (same as "Smythe") -->

<output><encode:metaphone>Knight</encode:metaphone></output>
<!-- NXT (silent 'k') -->
```

:::note
Metaphone is more accurate than Soundex for English text. Prefer `encode:metaphone` for English-language applications.
:::

[encode:soundex reference →](https://www.zeyos.com/ixml/ref/encode-soundex) | [encode:metaphone reference →](https://www.zeyos.com/ixml/ref/encode-metaphone)

## Compression

### encode:deflate / decode:deflate

Compress and decompress data using the DEFLATE algorithm (RFC 1951). The `level` attribute (0-9) controls the compression/speed trade-off.

```xml
<encode:deflate var="compressed" level="9">$large_text</encode:deflate>
<decode:deflate var="original">$compressed</decode:deflate>
```

:::note
DEFLATE produces raw compressed data without headers or checksums. Use `encode:zlib` for compression with error detection.
:::

[encode:deflate reference →](https://www.zeyos.com/ixml/ref/encode-deflate) | [decode:deflate reference →](https://www.zeyos.com/ixml/ref/decode-deflate)

### encode:zlib / decode:zlib

Compress and decompress using the ZLIB format (RFC 1950), which wraps DEFLATE with headers and an Adler-32 checksum for error detection.

```xml
<encode:zlib var="compressed" level="6">$data</encode:zlib>
<decode:zlib var="original">$compressed</decode:zlib>
```

[encode:zlib reference →](https://www.zeyos.com/ixml/ref/encode-zlib) | [decode:zlib reference →](https://www.zeyos.com/ixml/ref/decode-zlib)

## Encryption

### encode:crypt / decode:crypt

Symmetric-key encryption and decryption. Requires a `cipher` (e.g. `aes-256-cbc`), `key`, and stores/consumes an initialization vector (IV).

```xml
<!-- Encrypt -->
<encode:crypt var="ciphertext" var_iv="iv" cipher="aes-256-cbc" key="$encryption_key">
    $plaintext
</encode:crypt>

<!-- Decrypt -->
<decode:crypt var="plaintext" cipher="aes-256-cbc" key="$encryption_key" iv="$iv">
    $ciphertext
</decode:crypt>
```

:::warning
Never hardcode encryption keys in source code. Store IVs alongside ciphertext -- they are required for decryption. Use authenticated encryption modes (GCM) when possible.
:::

[encode:crypt reference →](https://www.zeyos.com/ixml/ref/encode-crypt) | [decode:crypt reference →](https://www.zeyos.com/ixml/ref/decode-crypt)

## Character Escaping

### encode:xml / decode:xml

Encode/decode XML entities (`&` `"` `<` `>`). Always XML-encode user-supplied content before embedding in XML documents.

```xml
<output><decode:xml>My name is &quot;iXML&quot; &amp; I am a XML derivative!</decode:xml></output>
<!-- My name is "iXML" & I am a XML derivative! -->
```

[encode:xml reference →](https://www.zeyos.com/ixml/ref/encode-xml) | [decode:xml reference →](https://www.zeyos.com/ixml/ref/decode-xml)

### encode:html / decode:html

Like XML encoding but also converts newlines to `<br />` and preserves spacing with `&nbsp;`. `decode:html` strips HTML tags and decodes entities.

```xml
<output><encode:html>$user_comment</encode:html></output>
```

:::warning
Always HTML-encode user-supplied content to prevent XSS attacks.
:::

[encode:html reference →](https://www.zeyos.com/ixml/ref/encode-html) | [decode:html reference →](https://www.zeyos.com/ixml/ref/decode-html)

### encode:url / decode:url

Percent-encode/decode text for safe use in URLs (RFC 1738).

```xml
<set var="url">https://example.com/search?q=<encode:url>$search_query</encode:url></set>
```

[encode:url reference →](https://www.zeyos.com/ixml/ref/encode-url) | [decode:url reference →](https://www.zeyos.com/ixml/ref/decode-url)

## Transfer Encoding

### encode:base64 / decode:base64

Encode/decode data using MIME Base64 (RFC 2045). Used for embedding binary data in text formats like JSON, XML, and email.

```xml
<output><encode:base64>My name is iXML!</encode:base64></output>
<!-- TXkgbmFtZSBpcyBpWE1MIQ== -->

<output><decode:base64>SGVsbG8gV29ybGQ=</decode:base64></output>
<!-- Hello World -->
```

[encode:base64 reference →](https://www.zeyos.com/ixml/ref/encode-base64) | [decode:base64 reference →](https://www.zeyos.com/ixml/ref/decode-base64)

### encode:quotedprint / decode:quotedprint

Quoted-printable encoding (RFC 2045) for mostly-ASCII text. More efficient than Base64 when data is primarily printable characters.

```xml
<output><encode:quotedprint>Hello World!
My name is iXML!</encode:quotedprint></output>
<!-- Hello World!=0AMy name is iXML! -->
```

[encode:quotedprint reference →](https://www.zeyos.com/ixml/ref/encode-quotedprint) | [decode:quotedprint reference →](https://www.zeyos.com/ixml/ref/decode-quotedprint)

## Structured Data Formats

### encode:json / decode:json

Convert between iXML variables and JSON (RFC 4627). Associative arrays become JSON objects; numeric arrays become JSON arrays. Use `pretty="true"` for formatted output.

```xml
<!-- Encode -->
<array var="response">
    <set var="status">success</set>
    <set var="message">User created</set>
</array>
<output><encode:json var="response"/></output>

<!-- Decode -->
<decode:json var="data">{"status":"success","user":{"id":123,"name":"John"}}</decode:json>
<output>$data.user.name</output>
<!-- John -->
```

:::warning
`decode:json` must use the `var` attribute. Without it, the result is an unusable internal reference.
:::

[encode:json reference →](https://www.zeyos.com/ixml/ref/encode-json) | [decode:json reference →](https://www.zeyos.com/ixml/ref/decode-json)

### encode:csv / decode:csv

Encode/decode comma-separated values. Input/output is a multidimensional array (rows of columns). Default delimiter is `;`.

```xml
<decode:csv var="data" delimiter=";">
First Name;Last Name
Bill;Gates
Steve;Jobs
</decode:csv>

<foreach var="data" var_value="row">
    <output>$row.0 $row.1</output>
</foreach>
```

[encode:csv reference →](https://www.zeyos.com/ixml/ref/encode-csv) | [decode:csv reference →](https://www.zeyos.com/ixml/ref/decode-csv)

### encode:binary / decode:binary

Pack/unpack array values into binary data using Perl/PHP-compatible format strings. Used for binary protocols and file formats.

```xml
<array var="packet">
    <item>0x01</item>
    <item>42</item>
    <item>1024</item>
</array>
<encode:binary var="packet" var_result="binary_packet" format="CCN"/>
```

[encode:binary reference →](https://www.zeyos.com/ixml/ref/encode-binary) | [decode:binary reference →](https://www.zeyos.com/ixml/ref/decode-binary)

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/standard-library/excel-zip

# Excel and ZIP

iXML provides dedicated namespaces for working with Excel spreadsheets (`excel`) and ZIP archives (`zip`).

## Excel Workbooks

### excel:workbook

Creates a new workbook or opens an existing one. All Excel operations must be nested inside this element.

```xml
<!-- Create a new workbook -->
<excel:workbook>
    <excel:set coords="A1" type="text">Name</excel:set>
    <excel:set coords="B1" type="numeric">36</excel:set>
    <excel:create var="book" format="xlsx"/>
</excel:workbook>

<!-- Open an existing file -->
<excel:workbook filename="./data/import.xlsx">
    <excel:get var="val" coords="A1"/>
</excel:workbook>
```

[Full reference →](https://www.zeyos.com/ixml/ref/excel-workbook)

## Cell Operations

### excel:set

Sets the value of a cell. Use `type` to control storage: `text`, `numeric`, `date`, `bool`, `null`, or `auto` (default).

```xml
<excel:set coords="A1" type="text">Product Name</excel:set>
<excel:set coords="B1" type="numeric" format="#,##0.00">49.99</excel:set>
<excel:set coords="C1" type="date">2024-01-15</excel:set>
```

:::tip
Omitting `type="numeric"` for numbers may store them as strings, causing Excel formulas to fail.
:::

[Full reference →](https://www.zeyos.com/ixml/ref/excel-set)

### excel:get

Gets the value of a cell. The `type` attribute controls the return: `raw` (default), `calculated`, or `formatted`.

```xml
<excel:get var="val" coords="A1"/>
```

[Full reference →](https://www.zeyos.com/ixml/ref/excel-get)

## Cursor Navigation

### excel:setpos

Sets the cursor position by indices (0-based) or cell coordinates.

```xml
<excel:setpos sheet="0" row="5" col="0"/>
<excel:setpos coords="B3"/>
```

[Full reference →](https://www.zeyos.com/ixml/ref/excel-setpos)

### excel:getpos

Gets the current cursor position as coordinates and/or indices.

```xml
<excel:getpos var_coords="pos" var_sheet="s" var_row="r" var_col="c"/>
```

[Full reference →](https://www.zeyos.com/ixml/ref/excel-getpos)

### excel:next

Shifts the cursor by a given offset. Default advances one column.

```xml
<excel:next offset_row="1" offset_col="0"/>
```

[Full reference →](https://www.zeyos.com/ixml/ref/excel-next)

### excel:length

Gets the number of sheets, rows, and columns for the current cursor context.

```xml
<excel:length var_sheets="s" var_rows="r" var_cols="c"/>
```

[Full reference →](https://www.zeyos.com/ixml/ref/excel-length)

## Sheet Management

### excel:addsheet

Adds a new sheet to the workbook.

```xml
<excel:addsheet>Summary</excel:addsheet>
<excel:setpos sheet="1"/>
<excel:set coords="A1">Summary Data</excel:set>
```

[Full reference →](https://www.zeyos.com/ixml/ref/excel-addsheet)

### excel:removesheet

Removes a sheet by index (0-based).

[Full reference →](https://www.zeyos.com/ixml/ref/excel-removesheet)

## Row and Column Management

### excel:addrow / excel:removerow

Insert or remove rows. Accepts optional `index` and `height` attributes.

[Full reference: addrow →](https://www.zeyos.com/ixml/ref/excel-addrow) | [removerow →](https://www.zeyos.com/ixml/ref/excel-removerow)

### excel:addcol / excel:removecol

Insert or remove columns. Accepts optional `index` and `width` attributes.

[Full reference: addcol →](https://www.zeyos.com/ixml/ref/excel-addcol) | [removecol →](https://www.zeyos.com/ixml/ref/excel-removecol)

## Styling

### excel:style

Applies styling to a cell range. The style is defined as a nested array with keys for `font`, `alignment`, `borders`, `fill`, and `numberFormat`.

```xml
<array var="headerStyle">
    <array key="font">
        <item key="bold">1</item>
        <item key="size">12</item>
    </array>
    <array key="fill">
        <item key="fillType">solid</item>
        <array key="startColor"><item key="rgb">CCCCFF</item></array>
    </array>
    <array key="borders">
        <array key="bottom">
            <item key="borderStyle">thin</item>
        </array>
    </array>
</array>
<excel:style var="headerStyle">A1:B1</excel:style>
```

[Full reference →](https://www.zeyos.com/ixml/ref/excel-style)

### excel:autofilter

Enables auto-filter dropdown menus on a header range.

```xml
<excel:autofilter>A1:D100</excel:autofilter>
```

[Full reference →](https://www.zeyos.com/ixml/ref/excel-autofilter)

## Data Export

### excel:array

Extracts all cell values into a 3D array: `data[sheet][row][col]`.

```xml
<excel:array var="data"/>
<output>$data[0][0][0]</output>  <!-- First sheet, first row, first col -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/excel-array)

### excel:create

Generates the workbook as binary data. Supported formats: `csv`, `ods`, `xls`, `xlsx`.

```xml
<excel:create var="book" format="xlsx"/>

<!-- Serve as download -->
<header>Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet</header>
<header>Content-Disposition: attachment; filename="report.xlsx"</header>
<output>$book</output>
```

[Full reference →](https://www.zeyos.com/ixml/ref/excel-create)

## Practical Excel Patterns

### Data Export with Styling

```xml
<excel:workbook title="Contact Export" subject="Monthly Report">
    <!-- Header row -->
    <excel:set coords="A1" type="text">Name</excel:set>
    <excel:set coords="B1" type="text">Email</excel:set>
    <excel:set coords="C1" type="text">Company</excel:set>
    <excel:set coords="D1" type="text">Revenue</excel:set>

    <!-- Style header -->
    <array var="hdr">
        <array key="font"><item key="bold">1</item><array key="color"><item key="rgb">FFFFFF</item></array></array>
        <array key="fill">
            <item key="fillType">solid</item>
            <array key="startColor"><item key="rgb">4472C4</item></array>
        </array>
    </array>
    <excel:style var="hdr">A1:D1</excel:style>

    <!-- Data rows from database -->
    <set var="row">2</set>
    <db:select var="contacts" entity="contacts" limit="100" type="assoc">
        <db:fields>c.name, c.email, c.company, c.volume</db:fields>
    </db:select>
    <foreach var="contacts" var_value="c">
        <excel:set coords="A$row" type="text">$c.name</excel:set>
        <excel:set coords="B$row" type="text">$c.email</excel:set>
        <excel:set coords="C$row" type="text">$c.company</excel:set>
        <excel:set coords="D$row" type="numeric" format="#,##0.00">$c.volume</excel:set>
        <set var="row">$(($row + 1))</set>
    </foreach>

    <excel:autofilter>A1:D$row</excel:autofilter>
    <excel:create var="book" format="xlsx"/>
</excel:workbook>
```

### Reading an Uploaded Excel File

```xml
<excel:workbook filename="$uploaded_file" format="xlsx">
    <excel:array var="data" type="raw"/>
    <excel:length var_rows="total_rows"/>
</excel:workbook>

<!-- Process rows (skip header at index 0) -->
<set var="imported">0</set>
<for var="i" from="1" to="$total_rows">
    <set var="name">$data[0][$i][0]</set>
    <set var="email">$data[0][$i][1]</set>

    <if value1="$name" func="!=">
        <set var="imported">$(($imported + 1))</set>
    </if>
</for>
<output>Imported $imported records</output>
```

---

## ZIP Archives

The `zip` namespace provides commands for creating and reading ZIP archives. All operations (except `zip:archive` itself) must be nested inside a `zip:archive` element. Changes are committed when `</zip:archive>` is reached.

### zip:archive

Creates a new archive or opens an existing one.

```xml
<zip:archive filename="./data/export.zip">
    <zip:write filename="readme.txt">This is the readme.</zip:write>
    <zip:write filename="data/report.csv">Name;Age&n;Ada;36</zip:write>
</zip:archive>
```

[Full reference →](https://www.zeyos.com/ixml/ref/zip-archive)

### zip:write

Writes content to an entry in the archive, creating or overwriting it.

[Full reference →](https://www.zeyos.com/ixml/ref/zip-write)

### zip:read

Reads the content of an archive entry into a variable.

```xml
<zip:archive filename="./data/export.zip">
    <zip:read var="readme" filename="readme.txt"/>
</zip:archive>
<output>$readme</output>
```

[Full reference →](https://www.zeyos.com/ixml/ref/zip-read)

### zip:exists

Checks whether an entry exists within the archive.

[Full reference →](https://www.zeyos.com/ixml/ref/zip-exists)

### zip:status

Returns status information (name, size, compressed size, CRC, mtime) about an entry.

[Full reference →](https://www.zeyos.com/ixml/ref/zip-status)

### zip:delete

Deletes an entry from the archive.

[Full reference →](https://www.zeyos.com/ixml/ref/zip-delete)

### zip:rename

Renames an entry within the archive.

[Full reference →](https://www.zeyos.com/ixml/ref/zip-rename)

### zip:list

Lists all entry filenames in the archive.

```xml
<zip:archive filename="./data/export.zip">
    <zip:list var="entries"/>
</zip:archive>
<foreach var="entries" var_value="name">
    <output>$name&n;</output>
</foreach>
```

[Full reference →](https://www.zeyos.com/ixml/ref/zip-list)

## Practical ZIP Pattern: Bundling Multiple Exports

```xml
<!-- Generate reports -->
<excel:workbook>
    <excel:set coords="A1">Report Data</excel:set>
    <excel:create var="excel_data" format="xlsx"/>
</excel:workbook>

<pdf:document var="pdf_data" title="Report">
    <pdf:section><pdf:body><pdf:block>Report PDF</pdf:block></pdf:body></pdf:section>
</pdf:document>

<!-- Bundle into ZIP -->
<zip:archive filename="./data/bundle.zip">
    <zip:write filename="report.xlsx">$excel_data</zip:write>
    <zip:write filename="report.pdf">$pdf_data</zip:write>
    <zip:write filename="metadata.json">{"generated": "$timestamp"}</zip:write>
</zip:archive>

<!-- Serve the ZIP -->
<file:read var="zipdata" filename="./data/bundle.zip"/>
<header>Content-Type: application/zip</header>
<header>Content-Disposition: attachment; filename="reports.zip"</header>
<output>$zipdata</output>
```

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/standard-library/filesystem

# File & Storage Operations

ZeyOS provides three distinct storage mechanisms, each designed for different use cases:

| Storage | Persistence | IDs | Best for |
|---------|------------|-----|----------|
| `file` | Permanent | Path | Import/export files, configuration, structured data |
| `temp` | Session / auto-cleanup | String | Intermediate processing, PDF templates, transient data |
| `bin` | Permanent | Integer | Generated documents, cached exports, binary attachments |

---

## File System (`file` namespace)

The `file` namespace provides commands for reading, writing, and managing files and directories within the application sandbox.

:::warning
File system access is restricted to the application's sandbox. Operations outside the sandbox will fail.
:::

### Reading Files

#### file:read

Reads the entire content of a file into a variable.

```xml
<file:read var="content" filename="./data/config.json"/>
<output>$content</output>
```

[Full reference →](https://www.zeyos.com/ixml/ref/file-read)

#### file:readcsv

Reads a CSV file into a multidimensional array (`rows[row][col]`). The default delimiter is `;` (semicolon) -- specify `delimiter=","` for standard comma-separated files.

```xml
<file:readcsv var="rows" filename="./data/contacts.csv" delimiter=","/>
<foreach var="rows" var_value="row">
    <output>$row[0] - $row[1]&n;</output>
</foreach>
```

[Full reference →](https://www.zeyos.com/ixml/ref/file-readcsv)

#### file:readjson

Reads a file and parses its content as JSON into a variable.

```xml
<file:readjson var="config" filename="./settings/config.json"/>
<output>API Key: $config.apiKey</output>
```

[Full reference →](https://www.zeyos.com/ixml/ref/file-readjson)

### Writing Files

#### file:write

Writes content to a file, creating or overwriting it.

```xml
<file:write filename="./data/output.txt">Report generated at $timestamp</file:write>
```

[Full reference →](https://www.zeyos.com/ixml/ref/file-write)

#### file:writecsv

Writes a multidimensional array as CSV to a file.

```xml
<array var="rows">
    <array><item>Name</item><item>Email</item></array>
    <array><item>Ada</item><item>ada@example.com</item></array>
</array>
<file:writecsv var="rows" filename="./data/export.csv" delimiter=","/>
```

[Full reference →](https://www.zeyos.com/ixml/ref/file-writecsv)

#### file:writejson

Writes the JSON representation of a variable to a file.

```xml
<array var="config">
    <item key="version">2</item>
    <item key="debug">false</item>
</array>
<file:writejson var="config" filename="./data/config.json"/>
```

[Full reference →](https://www.zeyos.com/ixml/ref/file-writejson)

#### file:append

Appends content to a file without overwriting. Creates the file if it does not exist.

```xml
<file:append filename="./logs/app.log">[$timestamp] $message&n;</file:append>
```

[Full reference →](https://www.zeyos.com/ixml/ref/file-append)

### File Information

#### file:exists

Checks whether a file or directory exists. Returns the canonicalized path on success, or `FALSE` if not found.

```xml
<file:exists var="exists" filename="./data/config.json"/>
<is var="exists" type="false">
    <output>File does not exist!</output>
</is>
```

[Full reference →](https://www.zeyos.com/ixml/ref/file-exists)

#### file:typeof

Returns the type of a file system entry: `file`, `dir`, `link`, or `undefined`.

```xml
<file:typeof var="type" filename="./data"/>
<output>Type: $type</output>
```

[Full reference →](https://www.zeyos.com/ixml/ref/file-typeof)

#### file:status

Returns detailed status information (size, timestamps, etc.) about a file or directory.

```xml
<file:status var="info" filename="./data/report.pdf"/>
<output>Size: $info.size bytes, Modified: $info.mtime</output>
```

[Full reference →](https://www.zeyos.com/ixml/ref/file-status)

#### file:pathinfo

Parses a filename into components: `dirname`, `basename`, `extension`, and `filename`.

```xml
<file:pathinfo var="info" filename="./data/report.xlsx"/>
<output>Dir: $info.dirname, Ext: $info.extension, Name: $info.filename</output>
```

[Full reference →](https://www.zeyos.com/ixml/ref/file-pathinfo)

### File Management

#### file:delete

Deletes a file. This operation is irreversible.

```xml
<file:exists var="exists" filename="./data/temp.txt"/>
<is var="exists" type="non-false">
    <file:delete filename="./data/temp.txt"/>
</is>
```

[Full reference →](https://www.zeyos.com/ixml/ref/file-delete)

#### file:rename

Renames or moves a file or directory.

```xml
<file:rename oldname="./data/draft.txt" newname="./data/final.txt"/>
```

[Full reference →](https://www.zeyos.com/ixml/ref/file-rename)

#### file:link

Creates a symbolic link pointing to a target path.

[Full reference →](https://www.zeyos.com/ixml/ref/file-link)

#### file:send

Sends a file directly to the client as the HTTP response body (for serving downloads).

```xml
<file:send filename="./data/report.pdf"/>
```

[Full reference →](https://www.zeyos.com/ixml/ref/file-send)

### Directory Operations

#### file:listdir

Lists all filenames within a directory.

```xml
<file:listdir var="files" dirname="./data"/>
<foreach var="files" var_value="filename">
    <output>$filename&n;</output>
</foreach>
```

[Full reference →](https://www.zeyos.com/ixml/ref/file-listdir)

#### file:glob

Finds all pathnames matching a wildcard pattern (`?`, `*`, `[...]`).

```xml
<file:glob var="csvfiles" pattern="./data/*.csv"/>
<foreach var="csvfiles" var_value="path">
    <file:pathinfo var="info" filename="$path"/>
    <output>Found CSV: $info.filename&n;</output>
</foreach>
```

[Full reference →](https://www.zeyos.com/ixml/ref/file-glob)

#### file:makedir

Creates a new directory. Parent directories are created recursively.

```xml
<file:makedir dirname="./data/exports/2024"/>
```

[Full reference →](https://www.zeyos.com/ixml/ref/file-makedir)

#### file:removedir

Removes an empty directory. Delete all files within it first.

[Full reference →](https://www.zeyos.com/ixml/ref/file-removedir)

---

## Temporary Data Store (`temp` namespace)

The `temp` namespace provides temporary storage that persists across requests within the same application context. When you write data with `temp:write`, it returns a string ID that you use for all subsequent operations. Temporary files are automatically cleaned up by ZeyOS.

```xml
<!-- Write temporary data — returns an ID -->
<temp:write var="temp_id">Temporary content</temp:write>

<!-- Read it back -->
<temp:read var="content" id="$temp_id"/>

<!-- Get the filesystem path (essential for pdf:templates) -->
<temp:path var="filepath" id="$temp_id"/>

<!-- Delete when done -->
<temp:delete id="$temp_id"/>
```

### Using temp for PDF templates

The most common use of `temp:path` is bridging between iXML resources and the PDF engine. Since `pdf:templates` requires a filesystem path, you store resource content in temp and pass the path:

```xml
<include id="letterhead" type="resource" var="template_data"/>
<temp:write var="temp_id">$template_data</temp:write>
<temp:path var="template_path" id="$temp_id"/>

<pdf:document var="pdf">
    <pdf:section>
        <pdf:templates filename="$template_path">
            <pdf:template source="1" target="1"/>
        </pdf:templates>
        <pdf:body>
            <pdf:block>Content on letterhead</pdf:block>
        </pdf:body>
    </pdf:section>
</pdf:document>
```

[`temp:write`](https://www.zeyos.com/ixml/ref/temp-write) | [`temp:read`](https://www.zeyos.com/ixml/ref/temp-read) | [`temp:path`](https://www.zeyos.com/ixml/ref/temp-path) | [`temp:delete`](https://www.zeyos.com/ixml/ref/temp-delete)

---

## Binary Data Store (`bin` namespace)

The `bin` namespace provides permanent binary storage. Unlike `temp`, binary data persists indefinitely and uses integer IDs. Use it when you need to store generated files (PDFs, exports, images) that should be retrievable later.

```xml
<!-- Store binary data — returns an integer ID -->
<bin:write var="bin_id">$pdf_data</bin:write>

<!-- Retrieve later -->
<bin:read var="retrieved" id="$bin_id"/>
```

A common pattern is generating a PDF and storing it for later download:

```xml
<pdf:document var="pdf">
    <!-- ... PDF content ... -->
</pdf:document>

<!-- Store the generated PDF permanently -->
<bin:write var="doc_id">$pdf</bin:write>

<!-- Save the bin ID alongside the entity for later retrieval -->
<extdata:set entity="transactions" id="$transaction_id">
    <extdata:data field="invoice_pdf">$doc_id</extdata:data>
</extdata:set>
```

[`bin:write`](https://www.zeyos.com/ixml/ref/bin-write) | [`bin:read`](https://www.zeyos.com/ixml/ref/bin-read)

---

## Practical Patterns

### File-based Import Workflow

```xml
<!-- Find all CSV files in the import directory -->
<file:glob var="imports" pattern="./data/imports/*.csv"/>

<foreach var="imports" var_value="filepath">
    <file:pathinfo var="info" filename="$filepath"/>
    <output>Processing: $info.basename&n;</output>

    <!-- Read and parse the CSV -->
    <file:readcsv var="rows" filename="$filepath" delimiter=","/>

    <!-- Process rows (skip header) -->
    <set var="i">0</set>
    <foreach var="rows" var_value="row">
        <if value1="$i" value2="0" func="&gt;">
            <output>  Row $i: $row[0] - $row[1]&n;</output>
        </if>
        <set var="i">$(($i + 1))</set>
    </foreach>

    <!-- Archive processed file -->
    <file:rename oldname="$filepath" newname="./data/archive/$info.basename"/>
</foreach>
```

### JSON Configuration Loading

```xml
<file:exists var="exists" filename="./settings/config.json"/>
<is var="exists" type="false">
    <!-- Create default config -->
    <array var="config">
        <item key="pageSize">25</item>
        <item key="locale">en</item>
    </array>
    <file:writejson var="config" filename="./settings/config.json"/>
</is>
<is var="exists" type="non-false">
    <file:readjson var="config" filename="./settings/config.json"/>
</is>
<output>Page size: $config.pageSize</output>
```

### CSV Import with Database Insert

```xml
<file:readcsv var="rows" filename="import/contacts.csv" delimiter="," header="1"/>

<foreach var="rows" var_value="row">
    <db:set entity="contacts">
        <db:data field="firstname">$row.firstname</db:data>
        <db:data field="lastname">$row.lastname</db:data>
        <db:data field="email">$row.email</db:data>
    </db:set>
</foreach>
```

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/standard-library/http-rest

# REST and HTTP Operations

The iXML language provides facilities for building RESTful APIs and consuming HTTP services. REST operations cover two use cases: **REST server** for building APIs, and **REST client** for consuming external APIs. HTTP operations provide low-level request capabilities.

## REST Server (Building APIs)

### rest:server / rest:resource

[`rest:server`](https://www.zeyos.com/ixml/ref/rest-server) defines a REST server with route-based dispatch. `rest:resource` defines individual endpoints.

**Key concepts:**
- Route parameters use `:name` syntax (`/users/:id`)
- Variadic routes use `*name` (`/files/*path`)
- Query parameters are automatically extracted as local variables. For POST, form fields are used instead, and route parameters take priority
- Set HTTP status with `<set var="return">404</set>`
- Each handler executes in its own local context — use `<use>` to import parent variables

:::warning
The request can hide any global inside a handler, because locals take priority. `?AUTH[user]=1` creates a local `AUTH`. Bind every global you read with `<use>` as the first child of `rest:resource`. See [Bind globals inside `rest:resource`](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#bind-globals-inside-restresource).
:::

```xml
<rest:server>
    <!-- GET /person/:id -->
    <rest:resource route="/person/:id" method="GET">
        <use var="people" />

        <array:keyexists var="people" var_result="exists">$id</array:keyexists>

        <is var="exists" type="false">
            <set var="return">404</set>
            <return/>
        </is>

        <header>Content-Type: application/json</header>
        <output>
            <encode:json var="people[$id]"/>
        </output>
    </rest:resource>

    <!-- POST /person (JSON body) -->
    <rest:resource route="/person" method="POST" var_body="body">
        <try>
            <decode:json var="data">$body</decode:json>

            <if value1="$data.email" func="=" value2="">
                <set var="return">400</set>
                <header>Content-Type: application/json</header>
                <output>{"error": "Email is required"}</output>
                <return/>
            </if>

            <db:set entity="contacts" var="new_id" var_data="data"/>
            <set var="return">201</set>
            <header>Content-Type: application/json</header>
            <output>{"result": 1, "id": $new_id}</output>

        <catch var="error">
            <set var="return">400</set>
            <header>Content-Type: application/json</header>
            <output>{"error": "Invalid JSON"}</output>
        </catch>
        </try>
    </rest:resource>

    <!-- DELETE /person/:id -->
    <rest:resource route="/person/:id" method="DELETE">
        <use var="people" />

        <unset var="people[$id]"/>
        <header>Content-Type: application/json</header>
        <output>{"result": 1}</output>
    </rest:resource>
</rest:server>
```

### Accessing request data

```xml
<rest:resource route="/protected" method="GET" var_header="headers" var_body="body">
    <use var="database_connection"/>

    <!-- Path parameters become local variables -->
    <output>ID: $id</output>

    <!-- Query parameters become local variables -->
    <output>Search: $q</output>

    <!-- Request headers via var_header -->
    <output>Auth: $headers.Authorization</output>

    <!-- Request body via var_body -->
    <decode:json var="payload">$body</decode:json>
</rest:resource>
```

:::warning
**Output Buffering:** All output within `rest:server` is buffered. Do not send output before the `rest:server` block, as this will commit HTTP headers prematurely.
:::

---

## REST Client (Consuming APIs)

### rest:client / rest:bind

[`rest:client`](https://www.zeyos.com/ixml/ref/rest-client) establishes a connection context for an external API. [`rest:bind`](https://www.zeyos.com/ixml/ref/rest-bind) creates callable functions bound to endpoints.

```xml
<rest:client url="https://api.predic8.de/shop/v2" timeout="30">
    <rest:header>
        Content-Type: application/json
        Accept: application/json
    </rest:header>

    <rest:bind var="createProduct" method="POST">/products/</rest:bind>
    <rest:bind var="updateProduct" method="PATCH">/products/:id</rest:bind>
    <rest:bind var="getProduct" method="GET">/products/:id</rest:bind>
    <rest:bind var="deleteProduct" method="DELETE">/products/:id</rest:bind>
</rest:client>

<!-- Create a product -->
<call func="createProduct" var="response">
    <param>{"name": "Wildberries", "price": 4.99}</param>
</call>

<decode:json var="product">$response</decode:json>
<output>Created product ID: $product.id</output>

<!-- Update the product -->
<call func="updateProduct">
    <param>$product.id</param>
    <param>{"price": 5.99}</param>
</call>
```

**Parameter passing:**
- Path parameters: first N params fill `:param` placeholders in order
- Request body: remaining params become the body (POST, PUT, PATCH)
- Use `type="raw"` on `rest:bind` to get full response with `status`, `type`, `header`, `body`

---

## HTTP Operations

### http:request

[`http:request`](https://www.zeyos.com/ixml/ref/http-request) sends a low-level HTTP request with full control over headers, body, and response handling.

```xml
<!-- GET request -->
<http:request url="https://api.example.com/users/123" var="response"/>

<!-- POST with JSON -->
<http:request url="https://api.example.com/users" method="POST" var="response" var_info="info">
    <http:header>
        Content-Type: application/json
        Authorization: Bearer $api_token
    </http:header>
    <http:body>
        {"name": "Jane Doe", "email": "jane@example.com"}
    </http:body>
</http:request>

<!-- Check response status -->
<if value1="$info.status" func="=" value2="200">
    <decode:json var="user">$response</decode:json>
    <output>User: $user.name</output>
<else/>
    <output>Error: $info.status</output>
</else></if>
```

The `var_info` array contains: `status` (HTTP code), `type` (Content-Type), `header` (raw headers), `body` (response body).

### http:urlinfo

[`http:urlinfo`](https://www.zeyos.com/ixml/ref/http-urlinfo) parses a URL into its components (`scheme`, `host`, `port`, `path`, `query`, `fragment`, `args`):

```xml
<http:urlinfo var="info" url="https://example.com:8080/api/users?role=admin"/>
<output>Host: $info.host, Path: $info.path, Role: $info.args.role</output>
```

### http:query

[`http:query`](https://www.zeyos.com/ixml/ref/http-query) builds a URL-encoded query string from an associative array:

```xml
<array var="params">
    <set var="search">iXML programming</set>
    <set var="limit">10</set>
</array>

<http:query var="params" var_result="qs"/>
<output>$qs</output>
<!-- search=iXML+programming&limit=10 -->
```

---

## Response patterns

### Consistent JSON envelope

```xml
<header>Content-Type: application/json</header>

<!-- Success -->
<output>{"result": 1, "data": <encode:json var="payload"/>}</output>

<!-- Error -->
<set var="return">400</set>
<output>{"error": "Validation failed", "details": "$message"}</output>
```

### HTTP status codes

| Code | When to use |
|------|-------------|
| 200 | Successful GET, PUT, PATCH, DELETE |
| 201 | Successful POST creating a resource |
| 400 | Invalid input, malformed JSON |
| 401 | Missing or invalid authentication |
| 404 | Resource not found |
| 500 | Unexpected server error |

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/standard-library/math-datetime

# Mathematics and Date/Time

The **Math** namespace provides arithmetic, trigonometry, rounding, random numbers, base conversion, and number formatting. The **Date** namespace provides Unix timestamp operations: retrieving, creating, decomposing, parsing, and formatting dates.

:::note Timestamp model
All date/time operations use **Unix timestamps** — seconds since January 1, 1970 00:00:00 UTC. There is no separate Date type; dates are always numeric timestamps.
:::

---

## Basic arithmetic

### `math:abs` — Absolute value

Returns the non-negative magnitude of a number.

```xml
<output><math:abs>-13.74</math:abs></output>
<!-- 13.74 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/math-abs)

---

### `math:sign` — Extract sign

Returns `1` (positive), `0` (zero), or `-1` (negative).

```xml
<output><math:sign>-13.74</math:sign></output>
<!-- -1 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/math-sign)

---

### `math:inc` — Increment variable

Increments a variable in place by a given amount (default 1). This is the idiomatic way to build counters in iXML.

```xml
<set var="number">1</set>
<math:inc var="number" />
<output>$number</output>
<!-- 2 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/math-inc)

---

### `math:dec` — Decrement variable

Decrements a variable in place by a given amount (default 1).

```xml
<set var="number">2</set>
<math:dec var="number" />
<output>$number</output>
<!-- 1 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/math-dec)

---

### `math:mod` — Floating-point modulo

Returns the remainder of dividing `x` by `y`. Supports fractional values.

```xml
<output><math:mod x="5.72" y="1.38" /></output>
<!-- 0.2 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/math-mod)

---

## Exponential and logarithmic functions

### `math:exp` — Natural exponential

Raises Euler's number (*e*) to the given power.

```xml
<output><math:exp>10</math:exp></output>
<!-- 22026.465794807 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/math-exp)

---

### `math:ln` — Natural logarithm

Computes the base-*e* logarithm. Input must be positive.

```xml
<output><math:ln>22026.465794807</math:ln></output>
<!-- 10 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/math-ln)

---

### `math:log` — Logarithm with base

Computes the logarithm with a specified base (default 10).

```xml
<output><math:log base="2">1024</math:log></output>
<!-- 10 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/math-log)

---

### `math:pow` — Raise to power

Raises a value to the given exponent (default 2).

```xml
<output><math:pow exponent="10">2</math:pow></output>
<!-- 1024 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/math-pow)

---

### `math:sqrt` — Square root

Returns the square root of a non-negative value.

```xml
<output><math:sqrt>25</math:sqrt></output>
<!-- 5 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/math-sqrt)

---

### `math:hypot` — Hypotenuse

Computes sqrt(x^2 + y^2) with better numerical precision than doing it manually.

```xml
<output><math:hypot x="3" y="5" /></output>
<!-- 5.8309518948453 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/math-hypot)

---

## Constants

### `math:pi` — Circle constant

Returns the mathematical constant pi with full floating-point precision.

```xml
<output><math:pi /></output>
<!-- 3.1415926535898 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/math-pi)

---

## Trigonometric functions

All trigonometric functions operate in **radians**. Use `math:convert` to convert between degrees and radians.

### Standard trigonometric functions

| Command | Description |
|---|---|
| `math:sin` | Sine of an angle |
| `math:cos` | Cosine of an angle |
| `math:tan` | Tangent of an angle |

```xml
<output><math:sin>1</math:sin></output>
<!-- 0.8414709848079 -->
```

[`math:sin` reference →](https://www.zeyos.com/ixml/ref/math-sin) | [`math:cos` reference →](https://www.zeyos.com/ixml/ref/math-cos) | [`math:tan` reference →](https://www.zeyos.com/ixml/ref/math-tan)

### Inverse (arc) functions

| Command | Description |
|---|---|
| `math:asin` | Arc sine — returns radians (input in [-1, 1]) |
| `math:acos` | Arc cosine — returns radians (input in [-1, 1]) |
| `math:atan` | Arc tangent — returns radians in (-pi/2, pi/2) |
| `math:atan2` | Two-argument arc tangent — quadrant-aware, returns radians in (-pi, pi] |

```xml
<output><math:atan2 x="3" y="5" /></output>
<!-- 1.0303768265243 -->
```

[`math:asin` →](https://www.zeyos.com/ixml/ref/math-asin) | [`math:acos` →](https://www.zeyos.com/ixml/ref/math-acos) | [`math:atan` →](https://www.zeyos.com/ixml/ref/math-atan) | [`math:atan2` →](https://www.zeyos.com/ixml/ref/math-atan2)

### Hyperbolic functions

| Command | Description |
|---|---|
| `math:sinh` | Hyperbolic sine |
| `math:cosh` | Hyperbolic cosine |
| `math:tanh` | Hyperbolic tangent |
| `math:asinh` | Inverse hyperbolic sine |
| `math:acosh` | Inverse hyperbolic cosine (input >= 1) |
| `math:atanh` | Inverse hyperbolic tangent (input in (-1, 1)) |

```xml
<output><math:sinh>1</math:sinh></output>
<!-- 1.1752011936438 -->
```

[`math:sinh` →](https://www.zeyos.com/ixml/ref/math-sinh) | [`math:cosh` →](https://www.zeyos.com/ixml/ref/math-cosh) | [`math:tanh` →](https://www.zeyos.com/ixml/ref/math-tanh) | [`math:asinh` →](https://www.zeyos.com/ixml/ref/math-asinh) | [`math:acosh` →](https://www.zeyos.com/ixml/ref/math-acosh) | [`math:atanh` →](https://www.zeyos.com/ixml/ref/math-atanh)

---

## Rounding

### `math:round` — Round to precision

Rounds a value to a specified number of decimal places. Supports 10 rounding strategies via the `type` attribute. Default mode is `half-infinity` (ties round away from zero).

**Nearest-value modes:** `half-infinity` (default), `half-zero`, `half-up`, `half-down`, `half-even` (banker's rounding), `half-odd`.

**Directed modes:** `infinity` (away from zero), `zero` (truncate), `up` (ceiling), `down` (floor).

```xml
<output><math:round precision="2">17.213482971852</math:round></output>
<!-- 17.21 -->
```

For financial calculations, use `half-even` to minimize cumulative rounding error:

```xml
<output><math:round precision="2" type="half-even">2.555</math:round></output>
<!-- 2.56 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/math-round)

---

### `math:ceil` — Ceiling

Rounds a value **up** to the next highest integer.

```xml
<output><math:ceil>17.213482971852</math:ceil></output>
<!-- 18 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/math-ceil)

---

### `math:floor` — Floor

Rounds a value **down** to the next lowest integer.

```xml
<output><math:floor>17.213482971852</math:floor></output>
<!-- 17 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/math-floor)

---

## Random numbers

### `math:rand` — Generate random number

Generates a random integer. Optionally specify `min` and `max` for a range (inclusive).

```xml
<set var="diceRoll"><math:rand min="1" max="6" /></set>
<output>You rolled: $diceRoll</output>
```

[Full reference →](https://www.zeyos.com/ixml/ref/math-rand)

---

## Number conversion

### `math:convert` — Convert number representation

Converts between number bases and angular units. Supported representations: `bin`, `oct`, `dec` (default), `hex`, `deg`, `rad`.

```xml
<!-- Hex to binary -->
<output><math:convert from="hex" to="bin">7cd5a72c</math:convert></output>
<!-- 1111100110101011010011100101100 -->
```

Particularly useful for converting degrees to radians before trigonometric operations:

```xml
<output>
    sin(45deg) = <math:sin><math:convert from="deg" to="rad">45</math:convert></math:sin>
</output>
<!-- sin(45deg) = 0.70710678118655 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/math-convert)

---

## Number formatting

### `math:format` — Format number for display

Produces a display string with fixed decimal places, configurable decimal point (`decpoint`), and thousands separator (`separator`). Returns a **string**, not a number -- do not use the result in further arithmetic.

```xml
<output><math:format countdec="2">1792702.213482971852</math:format></output>
<!-- 1,792,702.21 -->
```

German locale example:

```xml
<output><math:format countdec="2" decpoint="," separator=".">1792702.213482971852</math:format></output>
<!-- 1.792.702,21 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/math-format)

---

## Date and time

### `date:now` — Current timestamp

Returns the current Unix timestamp. Use `micro="true"` for microsecond precision (returns a float).

```xml
<date:now var="timestamp" />
<output>Current timestamp: $timestamp</output>
```

[Full reference →](https://www.zeyos.com/ixml/ref/date-now)

---

### `date:info` — Decompose timestamp

Breaks a timestamp into components: `day`, `month`, `year`, `hour`, `minute`, `second`, `wday` (0=Sunday..6=Saturday), `yday`. If no timestamp given, uses the current time.

```xml
<date:info var="now" />
<output>Today is $now.year-$now.month-$now.day</output>
```

Supports the `timezone` attribute for time zone-aware decomposition:

```xml
<date:info var="berlin" timezone="Europe/Berlin">946684800</date:info>
<output>In Berlin: $berlin.year-$berlin.month-$berlin.day $berlin.hour:$berlin.minute</output>
<!-- In Berlin: 2000-1-1 1:0 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/date-info)

---

### `date:create` — Create timestamp from components

Constructs a Unix timestamp from `day`, `month`, `year` (required) and optionally `hour`, `minute`, `second`, `timezone`. Returns `NULL` for invalid dates.

```xml
<output><date:create day="1" month="1" year="2000" /></output>
<!-- 946684800 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/date-create)

---

### `date:parse` — Parse date string

Converts a date string into a Unix timestamp. Accepts ISO 8601, common formats, and relative expressions. Returns `NULL` if unparseable. Use `timezone` to set the assumed time zone for strings without explicit zone info.

:::warning
Always verify parsed results. Ambiguous formats (e.g., `01/02/2025` -- is that Jan 2 or Feb 1?) may not parse as expected. Prefer ISO 8601 (`YYYY-MM-DD`) when possible.
:::

```xml
<output><date:parse>2000-01-01</date:parse></output>
<!-- 946684800 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/date-parse)

---

### `date:format` — Format timestamp

Converts a timestamp to a string using **PHP-compatible format characters**. If no timestamp given, uses the current time. Default format is `c` (ISO 8601).

**Common format characters:**

| Char | Output | Example |
|---|---|---|
| `Y` | 4-digit year | `2025` |
| `m` | Month (zero-padded) | `03` |
| `d` | Day (zero-padded) | `15` |
| `H` | Hour 24h (zero-padded) | `14` |
| `i` | Minute (zero-padded) | `30` |
| `s` | Second (zero-padded) | `00` |
| `c` | ISO 8601 full | `2025-03-15T14:30:00+01:00` |
| `D` | Day name (3-letter) | `Sat` |
| `F` | Month name (full) | `March` |

```xml
<output><date:format format="Y-m-d">946684800</date:format></output>
<!-- 2000-01-01 -->
```

European format with time zone:

```xml
<output><date:format format="d.m.Y H:i:s" timezone="Europe/Berlin">946684800</date:format></output>
<!-- 01.01.2000 01:00:00 -->
```

:::warning
Literal characters matching format codes must be backslash-escaped. For example, `\a\t` produces the text "at", not the `a` (am/pm) and `t` (days in month) format codes.
:::

[Full reference →](https://www.zeyos.com/ixml/ref/date-format)

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/standard-library/pdf

# PDF Generation

iXML provides a complete PDF generation engine through the `pdf` namespace. You can create multi-page documents with headers, footers, styled text, tables, images, barcodes, and imported templates -- all from XML markup.

## Document Structure

Every PDF follows a strict hierarchy:

```
pdf:document            <- document root (produces binary output)
├── pdf:file            <- embedded file attachment (PDF/A)
└── pdf:section         <- page layout definition (one or more)
    ├── pdf:templates   <- background template pages
    │   └── pdf:template <- individual template page mapping
    ├── pdf:header      <- repeated header content
    ├── pdf:footer      <- repeated footer content
    └── pdf:body        <- main page content
        ├── pdf:style   <- typography and layout styling
        ├── pdf:block   <- block-level text
        ├── pdf:inline  <- inline flow text
        ├── pdf:row     <- table row
        │   └── pdf:col <- table column
        ├── pdf:image   <- image
        ├── pdf:barcode <- barcode (1D or 2D)
        ├── pdf:linebreak <- vertical spacing
        ├── pdf:pagebreak <- force new page
        ├── pdf:include <- insert external PDF pages
        ├── pdf:getpos  <- read current position
        └── pdf:setpos  <- set current position
```

Content elements can appear inside `pdf:header`, `pdf:footer`, or `pdf:body`.

## Document Root

### pdf:document

Creates a PDF document. All sections and content are defined as children. The resulting binary is stored in the variable specified by `var`. Supports metadata attributes (`title`, `author`, `subject`, `keywords`), `pdfa` for PDF/A-3 compliance, `compress` and `subsetting` for size optimization, and `unit` for measurement (default `mm`).

[Full reference ->](https://www.zeyos.com/ixml/ref/pdf-document)

### pdf:signature

Adds a digital signature to the PDF document using PEM-encoded certificates and private keys. Child of `pdf:document` (single occurrence).

Full reference ->

### pdf:file

Embeds a file attachment in the PDF document. Primarily used with PDF/A-3 for structured data embedding (e.g., ZUGFeRD e-invoicing XML).

[Full reference ->](https://www.zeyos.com/ixml/ref/pdf-file)

## Page Layout

### pdf:section

Defines a page section with its own orientation, format, margins, header, footer, and body. A document can contain multiple sections with different page setups. Common formats: `A4`, `A3`, `A5`, `LETTER`, `LEGAL`.

[Full reference ->](https://www.zeyos.com/ixml/ref/pdf-section)

### pdf:templates

Defines a set of template pages to overlay as a background, loaded from an external PDF via `filename`. Contains `pdf:template` children mapping source pages to target pages. Commonly used for letterheads and branded backgrounds.

[Full reference ->](https://www.zeyos.com/ixml/ref/pdf-template)

### pdf:template

Maps a page from the template PDF to a target page in the generated document. Child of `pdf:templates`. Use `source` for the template page number and `target` for the output page number (omit `target` to apply to all pages).

[Full reference ->](https://www.zeyos.com/ixml/ref/pdf-template)

## Typography and Styling

### pdf:style

Defines visual styling for child content. Styles cascade -- each child inherits from its parent and overrides specific properties. Controls alignment, font (`font`, `fontsize`, `fontstyle`), colors (`textcolor`, `bgcolor`), borders (`border`, `borderwidth`, `bordercolor`), `lineheight`, `padding`, and `rotate`.

[Full reference ->](https://www.zeyos.com/ixml/ref/pdf-style)

Font style characters can be combined: `B` (bold), `I` (italic), `U` (underline), `O` (overline), `D` (line-through). For example, `fontstyle="BIU"` produces bold italic underlined text.

Border sides are specified as a string of characters: `"TBLR"` draws all four borders, `"TB"` draws top and bottom only.

## Text Content

### pdf:block

Prints a block of text. Each block starts on a new line and occupies the full available width. Supports `html` mode for basic HTML tags (`<b>`, `<i>`, `<u>`, `<br>`), absolute positioning via `x`/`y`, and explicit `width`/`height`.

[Full reference ->](https://www.zeyos.com/ixml/ref/pdf-block)

:::note
When using `html="t"`, HTML tags must be XML-escaped (e.g., `&lt;b&gt;` for `<b>`) because the content is parsed as XML first.
:::

### pdf:inline

Prints text as inline flow content. Unlike `pdf:block`, inline text does not start a new line -- multiple inline elements flow on the same line.

[Full reference ->](https://www.zeyos.com/ixml/ref/pdf-inline)

## Table Layout

### pdf:row

Defines a row of columns for tabular content. Row height adjusts to the tallest column. Contains one or more `pdf:col` children.

[Full reference ->](https://www.zeyos.com/ixml/ref/pdf-row)

### pdf:col

Defines a column within a `pdf:row`. Each column has its own `width`, and supports `html`, `nowrap`, and `height`.

[Full reference ->](https://www.zeyos.com/ixml/ref/pdf-col)

:::tip
Always set explicit `width` on every `pdf:col`. Without widths, columns expand to the right margin, making multi-column layouts unpredictable.
:::

## Media Content

### pdf:image

Inserts an image from a file path or URL. Supports `width`, `height`, `dpi`, and absolute positioning via `x`/`y`. When only one dimension is specified, the image scales proportionally.

[Full reference ->](https://www.zeyos.com/ixml/ref/pdf-image)

### pdf:barcode

Prints a 1D or 2D barcode. Supports QR codes (`QRCODE`, `QRCODE,M`, etc.), Code 128 (`C128`), EAN (`EAN8`, `EAN13`), DataMatrix, PDF417, and many more. The element content is the data to encode.

[Full reference ->](https://www.zeyos.com/ixml/ref/pdf-barcode)

## Page Flow Control

### pdf:linebreak

Adds vertical spacing by the given `offset` (in document units).

[Full reference ->](https://www.zeyos.com/ixml/ref/pdf-linebreak)

### pdf:pagebreak

Forces a new page. Optionally shifts the ordinate to a given `offset` on the new page.

[Full reference ->](https://www.zeyos.com/ixml/ref/pdf-pagebreak)

:::warning
`pdf:pagebreak` behaves like `pdf:linebreak` when used inside `pdf:header` or `pdf:footer`. It only creates a new page when used inside `pdf:body`.
:::

### pdf:include

Imports and inserts pages from an external PDF document. Use `from` and `to` to select a page range. Useful for appending terms and conditions or pre-designed content.

[Full reference ->](https://www.zeyos.com/ixml/ref/pdf-include)

:::note
`pdf:include` inserts pages as full content pages, while `pdf:templates` overlays a template as a background behind your generated content. `pdf:include` has no effect inside `pdf:header` or `pdf:footer`.
:::

## Position Control

### pdf:getpos

Reads the current drawing position. Stores the page number (`var_page`, `var_subpage`) and coordinates (`var_x`, `var_y`) into variables. Essential for dynamic layout control and page numbering in headers/footers.

[Full reference ->](https://www.zeyos.com/ixml/ref/pdf-getpos)

### pdf:setpos

Sets the current drawing position explicitly via `page`, `x`, and `y`. Enables absolute positioning and overlay effects.

[Full reference ->](https://www.zeyos.com/ixml/ref/pdf-setpos)

---

## Practical Patterns

### Minimal Document

```xml
<pdf:document var="pdf" title="Hello World">
    <pdf:section>
        <pdf:body>
            <pdf:block>Hello, PDF!</pdf:block>
        </pdf:body>
    </pdf:section>
</pdf:document>

<size var="len">$pdf</size>
<output>PDF size: $len bytes</output>
```

### Serving a PDF as HTTP Response

```xml
<pdf:document var="pdf" title="Invoice #1234" author="ZeyOS">
    <pdf:section format="A4">
        <pdf:body>
            <pdf:block>Invoice content here</pdf:block>
        </pdf:body>
    </pdf:section>
</pdf:document>

<header>Content-Type: application/pdf</header>
<header>Content-Disposition: attachment; filename="invoice-1234.pdf"</header>
<output>$pdf</output>
```

:::tip
Use `Content-Disposition: inline` to display the PDF in the browser instead of triggering a download.
:::

### Invoice with Line Items

This example demonstrates a realistic invoice layout combining most PDF commands:

```xml
<pdf:document var="pdf" title="Invoice" author="ZeyOS" compress="t" subsetting="t">
    <pdf:section format="A4" orientation="P"
        leftmargin="20" rightmargin="20" topmargin="25" bottommargin="20">
        <pdf:header>
            <pdf:style fontsize="8" textcolor="#999999" align="R">
                <pdf:getpos var_page="p"/>
                <pdf:block>Page $p</pdf:block>
            </pdf:style>
        </pdf:header>

        <pdf:body>
            <!-- Title -->
            <pdf:style fontsize="18" fontstyle="B">
                <pdf:block>INVOICE</pdf:block>
            </pdf:style>
            <pdf:linebreak offset="10"/>

            <!-- Customer info -->
            <pdf:style fontsize="10">
                <pdf:block>Customer: Ada Lovelace</pdf:block>
                <pdf:block>Date: 2024-01-15</pdf:block>
            </pdf:style>
            <pdf:linebreak offset="10"/>

            <!-- Table header -->
            <pdf:style fontsize="9" fontstyle="B" bgcolor="#F0F0F0"
                border="TB" borderwidth="0.5" padding="2">
                <pdf:row>
                    <pdf:col width="80">Description</pdf:col>
                    <pdf:col width="25">Qty</pdf:col>
                    <pdf:col width="35">Unit Price</pdf:col>
                    <pdf:col width="30">Total</pdf:col>
                </pdf:row>
            </pdf:style>

            <!-- Line items -->
            <pdf:style fontsize="9" padding="2">
                <pdf:row>
                    <pdf:col width="80">Enterprise License</pdf:col>
                    <pdf:col width="25">1</pdf:col>
                    <pdf:col width="35">1,200.00</pdf:col>
                    <pdf:col width="30">1,200.00</pdf:col>
                </pdf:row>
                <pdf:style bgcolor="#FAFAFA">
                    <pdf:row>
                        <pdf:col width="80">Support Package</pdf:col>
                        <pdf:col width="25">1</pdf:col>
                        <pdf:col width="35">300.00</pdf:col>
                        <pdf:col width="30">300.00</pdf:col>
                    </pdf:row>
                </pdf:style>
                <pdf:row>
                    <pdf:col width="80">Training Sessions</pdf:col>
                    <pdf:col width="25">3</pdf:col>
                    <pdf:col width="35">150.00</pdf:col>
                    <pdf:col width="30">450.00</pdf:col>
                </pdf:row>
            </pdf:style>

            <!-- Total -->
            <pdf:style fontsize="10" fontstyle="B" border="T" borderwidth="0.5" padding="2">
                <pdf:row>
                    <pdf:col width="140">Total</pdf:col>
                    <pdf:col width="30">1,950.00</pdf:col>
                </pdf:row>
            </pdf:style>

            <!-- QR code for payment -->
            <pdf:linebreak offset="15"/>
            <pdf:barcode type="QRCODE,M" width="30" height="30">PAY-INV-2024-001</pdf:barcode>
        </pdf:body>
    </pdf:section>
</pdf:document>
```

### Dynamic Table with Page Break Control

This pattern shows how to handle variable-length data with automatic page breaks:

```xml
<pdf:body>
    <foreach var="items" var_value="item">
        <!-- Check remaining space before each row -->
        <pdf:getpos var_y="y"/>
        <if value1="$y" value2="260" func="&gt;">
            <pdf:pagebreak/>
            <pdf:linebreak offset="5"/>

            <!-- Repeat table header on new page -->
            <pdf:style fontsize="9" fontstyle="B" border="B" borderwidth="0.5">
                <pdf:row>
                    <pdf:col width="80">Item</pdf:col>
                    <pdf:col width="30">Qty</pdf:col>
                    <pdf:col width="40">Price</pdf:col>
                </pdf:row>
            </pdf:style>
        </if>

        <pdf:style fontsize="9">
            <pdf:row>
                <pdf:col width="80">$item.name</pdf:col>
                <pdf:col width="30">$item.qty</pdf:col>
                <pdf:col width="40">$item.price</pdf:col>
            </pdf:row>
        </pdf:style>
    </foreach>
</pdf:body>
```

### Alternating Row Colors

```xml
<set var="row_index">0</set>
<foreach var="items" var_value="item">
    <set var="bg"></set>
    <if value1="$(($row_index % 2))" value2="0">
        <set var="bg">#F8F8F8</set>
    </if>

    <pdf:style fontsize="9" bgcolor="$bg" valign="M">
        <pdf:row>
            <pdf:col width="80">$item.name</pdf:col>
            <pdf:col width="40">$item.value</pdf:col>
        </pdf:row>
    </pdf:style>
    <set var="row_index">$(($row_index + 1))</set>
</foreach>
```

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/syntax-essentials

# Syntax Essentials

The Standard namespace (`std`) contains the operations you will use in virtually every iXML service. These commands handle assigning, inspecting, converting, and formatting data, as well as controlling execution flow.

## In this chapter

- **[Variables](https://www.zeyos.com/ixml/ixml-guide/syntax-essentials/variables)** — `set`, `unset`, `null`/`true`/`false`, `assign`, `clone`, `swap`, `cast`, `eval`, `verbatim`, `global`
- **[Type Introspection](https://www.zeyos.com/ixml/ixml-guide/syntax-essentials/type-introspection)** — `typeof`, `instanceof`, `classinfo`, `equals`
- **[String Operations](https://www.zeyos.com/ixml/ixml-guide/syntax-essentials/strings)** — `length`/`size`, `char`/`ord`, `concat`, `tolower`/`toupper`, `trim`, `pos`, `substr`, `pad`, `match`, `replace`, `split`, `convert`
- **[Output & Serialization](https://www.zeyos.com/ixml/ixml-guide/syntax-essentials/output)** — `output`, `header`, `sleep`, `serialize`/`unserialize`
- **[Conditionals](https://www.zeyos.com/ixml/ixml-guide/syntax-essentials/conditionals)** — `if`, `is`, `switch`
- **[Loops](https://www.zeyos.com/ixml/ixml-guide/syntax-essentials/loops)** — `for`, `foreach`, `while`, `repeat`/`until`
- **[Branching](https://www.zeyos.com/ixml/ixml-guide/syntax-essentials/branching)** — `break`, `continue`, `return`, `exit`

:::note
Documentation for `include` and `param` has been moved to **[Reusability and OOP](https://www.zeyos.com/ixml/ixml-guide/reusability-and-oop/functions)** to better align with function and class concepts.
:::

---

## Common pitfalls

- **`<set var="x">42</set>` creates a string**, not an integer. Use `<cast type="int"/>` for numeric behavior.
- **`<assign>` shares references** for arrays/functions. Use `<clone>` for independent copies.
- **`<length>` counts characters**, `<size>` counts bytes. Choose based on whether you're working with text or binary data.
- **XML escaping in attributes**: `<` must be `&lt;` and `>` must be `&gt;` in attribute values. This commonly catches comparison operators: `func="&lt;"`.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/syntax-essentials/branching

# Branching and Flow Control

## `break` — Break loop

[Full reference →](https://www.zeyos.com/ixml/ref/break)

Exits the current loop immediately.

### Syntax

```xml
<break />
<break level="2" /> <!-- Break out of 2 nested loops -->
```

---

## `continue` — Continue loop

Full reference →

Skips the rest of the current iteration and continues with the next one.

### Syntax

```xml
<continue />
<continue level="2" />
```

---

## `return` — Return from function

[Full reference →](https://www.zeyos.com/ixml/ref/return)

Exits the current function context.

### Syntax

```xml
<return />
<return>value</return> <!-- Return with value (implicit set) -->
```

---

## `exit` — Terminate execution

[Full reference →](https://www.zeyos.com/ixml/ref/exit)

Immediately halts the entire iXML execution script.

### Syntax

```xml
<exit />
```

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/syntax-essentials/conditionals

# Conditionals

## `if` — Conditional value execution

[Full reference →](https://www.zeyos.com/ixml/ref/if)

The `IF` command is the primary conditional construct in iXML. It evaluates a comparison and executes its content only if the result is true.

### Syntax

```xml
<if value1="$a" func="=" value2="$b">
    <!-- Executed if a == b -->
</if>
```

### Attributes

| Name | Type | Description |
|---|---|---|
| `value1` | string | Left operand. |
| `value2` | string | Right operand. |
| `func` | function | Comparison operator (default `=`). |

### Comparison Operators

| `func` | Description |
|---|---|
| `=` | Equals. |
| `!=` | Not equals. |
| `>` | Greater than. |
| `<` | Less than. |
| `>=` | Greater or equal. |
| `<=` | Less or equal. |
| `~` | Regex match. |
| `!~` | Regex non-match. |

---

## `is` — Conditional type check

[Full reference →](https://www.zeyos.com/ixml/ref/is)

The `IS` command checks the validity, type, or state of a single variable.

### Syntax

```xml
<is var="myVar" type="numeric">
    <!-- Executed if myVar is numeric -->
</is>
```

### Common Types

- `valid` (defined and not null/false)
- `empty` (null, false, empty string, empty array)
- `array`, `string`, `int`, `float`, `bool`
- `numeric`

---

## `switch` — Multi-way branching

[Full reference →](https://www.zeyos.com/ixml/ref/switch) | `case` reference → | `default` reference →

The `SWITCH` command simplifies chains of if-elseif statements comparing a single value against multiple options.

### Syntax

```xml
<switch value="$status">
    <case value="active">Active!</case>
    <case value="pending">Pending...</case>
    <default>Unknown status</default>
</switch>
```

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/syntax-essentials/loops

# Loops

## `foreach` — Iterate array

[Full reference →](https://www.zeyos.com/ixml/ref/foreach)

The `FOREACH` loop iterates over an array, assigning the current key and value to variables for each iteration.

### Syntax

```xml
<foreach var="arrayName" var_value="val" var_key="key">
    <output>Key: $key, Value: $val</output>
</foreach>
```

---

## `for` — Counting loop

[Full reference →](https://www.zeyos.com/ixml/ref/for)

The `FOR` loop iterates through a range of integers or steps.

### Syntax

```xml
<for var="i" from="1" to="10">
    <output>Count: $i</output>
</for>
```

---

## `while` — Conditional loop

[Full reference →](https://www.zeyos.com/ixml/ref/while)

The `WHILE` loop repeats as long as a condition is true.

### Syntax

```xml
<while value1="$count" func="&gt;" value2="0">
    <!-- loop body -->
</while>
```

---

## `repeat` / `until` — Post-test loop

Full reference → | Full reference →

The `REPEAT/UNTIL` loop always executes at least once and repeats until a condition becomes true.

### Syntax

```xml
<repeat>
    <!-- body -->
    <until value1="$x" func="=" value2="0" />
</repeat>
```

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/syntax-essentials/output

# Output & Serialization

## `output` — Send to response

The primary mechanism for producing HTTP response body content:

```xml
<output>Hello World!</output>
```

[Full reference →](https://www.zeyos.com/ixml/ref/output)

---

## `header` — Set HTTP header

Sets HTTP response headers and optionally the status code:

```xml
<header status="200">Content-Type: application/json</header>
```

Specifying a header without a value removes it from the response.

[Full reference →](https://www.zeyos.com/ixml/ref/header)

---

## `sleep` — Pause execution

Pauses for a specified duration in seconds (supports fractional seconds):

```xml
<sleep>0.5</sleep>
<!-- pauses for 500 milliseconds -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/sleep)

---

## `serialize` / `unserialize`

Converts variables to/from a storable string representation (PHP-compatible format). Preserves types and structure, unlike JSON which loses type information:

```xml
<array var="names">
    <item key="bg">Bill Gates</item>
    <item key="sj">Steve Jobs</item>
</array>

<serialize var="names" var_result="data"/>
<unserialize var="restored">$data</unserialize>
<output>$restored.bg</output>
<!-- Bill Gates -->
```

[`serialize`](https://www.zeyos.com/ixml/ref/serialize) | [`unserialize`](https://www.zeyos.com/ixml/ref/unserialize)

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/syntax-essentials/strings

# String Operations

## `length` / `size`

`length` returns character count (Unicode code points). `size` returns byte count. For ASCII text these are the same; for multibyte UTF-8 they differ.

```xml
<length var="len">iXML</length>
<output>$len</output>
<!-- 4 -->
```

[`length`](https://www.zeyos.com/ixml/ref/length) | [`size`](https://www.zeyos.com/ixml/ref/size)

---

## `char` / `ord`

Convert between characters and UTF-8 code points:

```xml
<output><char>65</char></output>   <!-- A -->
<output><ord>A</ord></output>      <!-- 65 -->
```

[`char`](https://www.zeyos.com/ixml/ref/char) | [`ord`](https://www.zeyos.com/ixml/ref/ord)

---

## `concat` — Append to string

Appends a value to an existing variable. More efficient than `<set var="x">$x$suffix</set>` in loops:

```xml
<set var="name">i</set>
<concat var="name">XML</concat>
<output>$name</output>
<!-- iXML -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/concat)

---

## `tolower` / `toupper`

Case conversion. `toupper` supports modes: `chars` (all), `words` (first letter of each word), `first` (first character only):

```xml
<output><tolower>iXML</tolower></output>
<!-- ixml -->

<output><toupper type="words">bill gates</toupper></output>
<!-- Bill Gates -->
```

[`tolower`](https://www.zeyos.com/ixml/ref/tolower) | [`toupper`](https://www.zeyos.com/ixml/ref/toupper)

---

## `trim` — Strip whitespace

Strips whitespace from both ends (default), left only, or right only:

```xml
<output><trim>     iXML     </trim></output>
<!-- iXML -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/trim)

---

## `pos` — Find position

Finds the position of a search value within a string:

```xml
<output><pos value="X">iXML</pos></output>
<!-- 1 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/pos)

---

## `substr` — Extract substring

Returns a portion of a string by offset and optional length:

```xml
<output><substr offset="6" length="2">Name: iXML</substr></output>
<!-- iX -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/substr)

---

## `pad` — Pad string

Pads to a given length. Positive length pads right; negative pads left:

```xml
<output><pad length="-10" padding="0">123</pad></output>
<!-- 0000000123 -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/pad)

---

## `match` — Regex match

Performs a global regex match, returning the count and match data:

```xml
<match var="count" var_matches="m" pattern="/ixml/i">My name is iXML!</match>
<output>Found $count match: $m[0][0]</output>
<!-- Found 1 match: iXML -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/match)

---

## `replace` — Search and replace

Supports both literal and regex replacement. Regex mode supports backreferences (`\0` full match, `\1` first capture group):

```xml
<!-- Literal replacement -->
<output>
    <replace value="Bill" replacement="William">Bill Gates</replace>
</output>
<!-- William Gates -->

<!-- Regex with backreferences -->
<output>
    <replace pattern="/(\w+) (\w+)/" replacement="\2, \1">Bill Gates</replace>
</output>
<!-- Gates, Bill -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/replace)

---

## `split` — Split to array

Splits a string into an array by delimiter or regex pattern:

```xml
<split var="names" delimiter=", ">Bill Gates, Steve Jobs</split>
<output>$names[0]</output>
<!-- Bill Gates -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/split)

---

## `convert` — Character encoding

Converts between character encodings (e.g., UTF-8 to ISO-8859-1).

[Full reference →](https://www.zeyos.com/ixml/ref/convert)

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/syntax-essentials/type-introspection

# Type Introspection

## `typeof` — Get variable type

Returns the intrinsic data type as a string (`'undefined'`, `'null'`, `'bool'`, `'int'`, `'float'`, `'string'`, `'array'`, `'function'`, `'macro'`, `'class'`):

```xml
<set var="name">iXML</set>
<typeof var="name" var_result="type"/>
<output>$type</output>
<!-- string -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/typeof)

---

## `instanceof` — Check class membership

Checks whether a variable is an instance of a given class or its subclasses:

```xml
<class var="Animal"/>
<class var="Dog">
    <extends class="Animal"/>
</class>

<new class="Dog" var="rex"/>
<instanceof class="Animal" var="rex" var_result="isAnimal"/>
<output>$isAnimal</output>
<!-- 1 (TRUE) -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/instanceof)

---

## `classinfo` — Reflect on class structure

Returns an array with class metadata (prototype, parents, constructors, inheritance chain).

[Full reference →](https://www.zeyos.com/ixml/ref/classinfo)

---

## `equals` — Strict equality

Checks strict equality **without type coercion** — the string `"123"` and integer `123` are not equal:

```xml
<set var="str">123</set>
<set var="num">123</set>
<cast var="num" type="int"/>

<equals var1="str" var2="num" var_result="eq"/>
<!-- eq is FALSE because types differ -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/equals)

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/syntax-essentials/variables

# Variables

## `set` — Set variable

The most fundamental command in iXML. Assigns a string value to a variable, creating it if it doesn't exist. The value is **always of type `string`** unless replaced by an embedded statement via result propagation.

```xml
<set var="firstname">Bill</set>
<set var="lastname">Gates</set>
<output>$firstname $lastname</output>
<!-- Bill Gates -->
```

Setting array items — these three forms are equivalent:

```xml
<set var="names[bg]">Bill Gates</set>
<set var="names.sj">Steve Jobs</set>
<set var="names" key="le">Larry Ellison</set>
```

Auto-increment key with empty brackets:

```xml
<set var="list[]">first</set>
<set var="list[]">second</set>
```

[Full reference →](https://www.zeyos.com/ixml/ref/set)

---

## `unset` — Remove variable

Removes a variable entirely, making it `undefined`. Different from setting to `NULL` or empty string.

```xml
<set var="x">hello</set>
<unset var="x"/>
<!-- typeof is now 'undefined' -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/unset)

---

## `null` / `true` / `false` — Set typed values

Set a variable to a specific typed value directly:

```xml
<null var="x"/>    <!-- type: null -->
<true var="flag"/> <!-- type: bool, value: TRUE -->
<false var="off"/> <!-- type: bool, value: FALSE -->
```

[`null`](https://www.zeyos.com/ixml/ref/null) | [`true`](https://www.zeyos.com/ixml/ref/true) | [`false`](https://www.zeyos.com/ixml/ref/false)

---

## `assign` — Type-preserving assignment

Copies a value from one variable to another, preserving the original data type. Complex types (arrays, functions) are assigned **by reference**.

```xml
<set var="name1">iXML</set>
<assign var="name2" var_source="name1"/>
<output>$name2</output>
<!-- iXML -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/assign)

---

## `clone` — Independent copy

Creates a **shallow copy** independent of the original. Essential for arrays where `assign` would create a shared reference.

```xml
<array var="original">
    <item>Bill Gates</item>
    <item>Steve Jobs</item>
</array>

<clone var="original" var_result="copy"/>
<set var="copy[1]">Larry Ellison</set>

<output>$original[1] and $copy[1]</output>
<!-- Steve Jobs and Larry Ellison -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/clone)

---

## `swap` — Exchange two variables

Exchanges values in a single atomic step:

```xml
<set var="a">first</set>
<set var="b">second</set>
<swap var1="a" var2="b"/>
<output>$a, $b</output>
<!-- second, first -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/swap)

---

## `cast` — Type conversion

Converts a variable to a specified data type. Essential because `<set>` always creates strings.

```xml
<set var="number">123</set>
<cast var="number" type="int"/>

<is var="number" type="int">
    <output>$number is now an integer!</output>
</is>
```

Supported types: `bool`, `int`, `float`, `string`. See [Type Conversion](https://www.zeyos.com/ixml/ixml-guide/data-types/type-conversion) for conversion rules.

[Full reference →](https://www.zeyos.com/ixml/ref/cast)

---

## `eval` — Evaluate expression

Evaluates a string with variable substitution, resolving references dynamically:

```xml
<set var="dollar">$</set>
<set var="name">iXML</set>

<output>
    <eval>My name is $dollar$name!</eval>
</output>
<!-- My name is iXML! -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/eval)

---

## `verbatim` — Prevent substitution

Returns content **without** variable substitution — the opposite of `eval`:

```xml
<output>
    <verbatim>Show me the $dollars!</verbatim>
</output>
<!-- Show me the $dollars! -->
```

[Full reference →](https://www.zeyos.com/ixml/ref/verbatim)

---

## `global` — Declare global variable

Declares a variable with global scope, visible across all contexts including functions and includes:

```xml
<global var="appName"/>
<set var="appName">MyApp</set>

<function var="showName">
    <output>$appName</output>
</function>

<call func="showName"/>
<!-- MyApp -->
```

Use sparingly — prefer passing data as parameters.

[Full reference →](https://www.zeyos.com/ixml/ref/global)

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/user-interface

# User Interface & Forms

ZeyOS applications can extend the platform's user interface in two ways:

1. **Form-based weblets** — Use ZeyOS's built-in `form` namespace to build structured data-entry forms declaratively. The form engine handles layout, input validation, and data persistence automatically. No HTML, CSS, or JavaScript required.
2. **Custom weblets** — Build fully custom UI components using HTML, CSS, and JavaScript. Custom weblets give you complete control over the interface and can interact with ZeyOS services via AJAX calls.

Both approaches run as **weblets** — embedded UI panels that appear within entity detail views (contacts, tickets, accounts, contracts, etc.). Weblets are defined in the `weblets/` directory of a ZeyOS application and are linked to specific entity types through the app manifest (`zeyos.app.json`).

A third special case is **exports**, which generate downloadable files (CSV, Excel, PDF) from the current view's selected records.

## In this chapter

- **[Weblets](https://www.zeyos.com/ixml/ixml-guide/user-interface/weblets)** — How weblets work, their lifecycle, and building custom HTML/JS interfaces
- **[Forms](https://www.zeyos.com/ixml/ixml-guide/user-interface/forms)** — The declarative form namespace for building native ZeyOS data-entry forms
- **[Exports](https://www.zeyos.com/ixml/ixml-guide/user-interface/exports)** — Generating exports from the current view's filtered and selected records

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/user-interface/exports

# Exports

Exports are a special type of weblet that generate downloadable data (CSV, Excel, PDF, etc.) from entity list views. What makes exports unique is the **tempfile selector** — a mechanism that passes the user's current view state (filters, search terms, and manual selections) into your iXML script, so the export contains exactly the records the user expects.

## How the tempfile selector works

When a user triggers an export from an entity list view, ZeyOS:

1. Collects all record IDs matching the current view — respecting active filters, search queries, and any manually selected rows
2. Writes those IDs into a temporary file
3. Passes the temp file's ID to your export script via `$REQUEST.tempfile`

Your script reads this selector and uses it to query exactly the right records:

```xml
<!-- Read the list of IDs from the tempfile -->
<temp:read id="$REQUEST.tempfile" var_struct="selector" />
```

The `var_struct="selector"` attribute parses the temp file content into a structure that can be used directly with [`db:in`](https://www.zeyos.com/ixml/ref/db-in) to filter a database query.

## Basic export pattern

The typical export follows three steps: read the selector, query the data, and format the output.

```xml
<!-- 1. Read the selector from the tempfile -->
<temp:read id="$REQUEST.tempfile" var_struct="selector" />

<!-- 2. Query the records matching the selection -->
<db:select var_result="rows" type="assoc">
    <db:fields>
        <db:field>a.lastname</db:field>
        <db:field>a.firstname</db:field>
        <db:field>t.date</db:field>
        <db:field>t.transactionnum</db:field>
        <db:field>t.status</db:field>
        <db:field>t.netamount</db:field>
    </db:fields>
    <db:table alias="t">transactions</db:table>
    <db:join>
        <db:inner alias="a" table="accounts" field1="t.account" field2="a.ID" />
    </db:join>
    <db:in field="a.ID" var="selector" />
    <db:orderby>
        <db:orderfield>a.lastname</db:orderfield>
    </db:orderby>
</db:select>

<!-- 3. Format and output (e.g., as CSV) -->
<set var="return.contenttype">text/csv</set>
<set var="return.filename">export.csv</set>
<output>Last Name,First Name,Date,Transaction No.,Status,Net Amount</output>
<foreach var="rows" var_value="row">
    <output>
$row.lastname,$row.firstname,$row.date,$row.transactionnum,$row.status,$row.netamount</output>
</foreach>
```

[`temp:read`](https://www.zeyos.com/ixml/ref/temp-read) | [`db:in`](https://www.zeyos.com/ixml/ref/db-in) | [`db:select`](https://www.zeyos.com/ixml/ref/db-select)

## The selector and `db:in`

The key integration point is `<db:in>` — it takes the selector variable and generates a SQL `IN (...)` clause that filters the query to only the selected IDs. This works with any entity field, not just `ID`:

```xml
<db:in field="t.ID" var="selector" />
```

You can combine `db:in` with additional `db:is` conditions to further narrow results:

```xml
<db:select var_result="rows" type="assoc">
    <db:fields>
        <db:field>t.transactionnum</db:field>
        <db:field>t.netamount</db:field>
    </db:fields>
    <db:table alias="t">transactions</db:table>
    <db:in field="t.ID" var="selector" />
    <db:is field="t.status">2</db:is>
</db:select>
```

## Excel export

For structured exports, use the [Excel namespace](https://www.zeyos.com/ixml/ixml-guide/standard-library/excel-zip) to generate `.xlsx` files:

```xml
<temp:read id="$REQUEST.tempfile" var_struct="selector" />

<db:select var_result="contacts" type="assoc">
    <db:fields>
        <db:field>lastname</db:field>
        <db:field>firstname</db:field>
        <db:field>email</db:field>
        <db:field>phone</db:field>
    </db:fields>
    <db:table>contacts</db:table>
    <db:in field="ID" var="selector" />
    <db:orderby>
        <db:orderfield>lastname</db:orderfield>
    </db:orderby>
</db:select>

<excel:create var="wb">
    <excel:sheet title="Contacts">
        <!-- Header row -->
        <excel:row>
            <excel:cell style="bold">Last Name</excel:cell>
            <excel:cell style="bold">First Name</excel:cell>
            <excel:cell style="bold">Email</excel:cell>
            <excel:cell style="bold">Phone</excel:cell>
        </excel:row>
        <!-- Data rows -->
        <foreach var="contacts" var_value="c">
            <excel:row>
                <excel:cell>$c.lastname</excel:cell>
                <excel:cell>$c.firstname</excel:cell>
                <excel:cell>$c.email</excel:cell>
                <excel:cell>$c.phone</excel:cell>
            </excel:row>
        </foreach>
    </excel:sheet>
</excel:create>

<set var="return.contenttype">application/vnd.openxmlformats-officedocument.spreadsheetml.sheet</set>
<set var="return.filename">contacts_export.xlsx</set>
<output>$wb</output>
```

## Tips

- The selector respects whatever the user sees in their view — if they've filtered by status, date range, or search term, only matching IDs are included.
- If the user manually selected specific rows (via checkboxes), only those IDs are passed.
- Always use `var_struct="selector"` with `temp:read` so the IDs are in the correct format for `db:in`.
- Set `return.contenttype` and `return.filename` to control the download behavior in the browser.

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/user-interface/forms

# Forms

The `form` namespace provides a declarative UI toolkit for building structured data-entry forms inside ZeyOS **weblets**. Layout containers and input controls render natively in the ZeyOS interface — no HTML or CSS required. The form namespace only works in weblets; using it in services, resources, or standalone `zeysdk run` produces no visible output.

When a form is submitted, each control's `id` attribute becomes a key in the entity's extended data (`EXTDATA`). Use `$EXTDATA.fieldId` to read previously submitted values on subsequent loads.

## In this chapter

- `hbox` and `vbox` — horizontal and vertical layout containers
- `placeholder` — empty spacer element
- `title`, `description`, `link`, `image`, `rule` — display elements
- `textbox` — text input with 17 type variants
- `checkbox` — two-state toggle
- `listbox` and `listitem` — single and multi-select lists
- `entitybox` — ZeyOS entity record selector

---

## Layout containers

### `hbox` — Horizontal box

Arranges child form elements side by side in a row. The most common use is placing two or more fields on one line, or creating multi-column layouts with nested `vbox` containers. Accepts optional `align`, `width`, `height`, and `padding` attributes.

Full reference →

```xml
<hbox>
    <textbox id="firstname" label="First Name" width="300" />
    <textbox id="lastname" label="Last Name" width="300" />
</hbox>
```

Multi-column layout with `vbox` nesting:

```xml
<hbox>
    <vbox width="50%">
        <textbox id="company" label="Company" width="300" />
        <textbox id="email" label="Email" type="email" width="300" />
    </vbox>
    <vbox width="50%">
        <textbox id="phone" label="Phone" type="tel" width="300" />
        <textbox id="website" label="Website" type="url" width="300" />
    </vbox>
</hbox>
```

---

### `vbox` — Vertical box

Stacks child form elements vertically. Use it inside an `hbox` to create columns, or standalone to control width and padding. Accepts optional `align`, `width`, `height`, and `padding` attributes.

Full reference →

```xml
<hbox>
    <vbox width="25%">
        <title>Locations</title>
        <foreach var="config.locations" var_value="location">
            <checkbox id="loc_$location" caption="$location">$location</checkbox>
        </foreach>
    </vbox>
    <vbox width="25%">
        <title>Departments</title>
        <foreach var="config.departments" var_value="dept">
            <checkbox id="dept_$dept" caption="$dept">$dept</checkbox>
        </foreach>
    </vbox>
    <vbox width="25%">
        <textbox id="date_from" label="From" type="date" width="200" />
    </vbox>
    <vbox width="25%">
        <textbox id="date_to" label="To" type="date" width="200" />
    </vbox>
</hbox>
```

---

### `placeholder` — Empty placeholder

Reserves empty space in a layout. Use it to balance columns when one side of an `hbox` has fewer controls, or to insert spacing between sections. Accepts optional `width` and `height` attributes.

Full reference →

```xml
<hbox>
    <textbox id="notes" type="multi" label="Additional Notes" />
    <placeholder />
</hbox>
```

---

## Display elements

### `title` — Section title

Renders an emphasized heading that visually separates form sections.

Full reference →

```xml
<title>Contact Information</title>
<hbox>
    <textbox id="email" label="Email" type="email" width="300" />
    <textbox id="phone" label="Phone" type="tel" width="300" />
</hbox>

<rule />

<title>Address</title>
<textbox id="street" label="Street" width="400" />
<hbox>
    <textbox id="zip" label="ZIP" width="100" />
    <textbox id="city" label="City" width="300" />
</hbox>
```

---

### `description` — Labeled description

Displays read-only text attached to a label. Use it to show computed values, status information, or messages that the user cannot edit. Accepts an optional `label` attribute.

Full reference →

```xml
<description label="Created">
    <date:format format="d.m.Y">$EXTDATA.created</date:format>
</description>
<description label="Created by">$EXTDATA.creator_name</description>
<description label="Status">$EXTDATA.status</description>
```

Summary row with computed values:

```xml
<hbox>
    <description label="Total Net">$formatNum($totalNet)</description>
    <description label="Total Gross">$formatNum($totalGross)</description>
    <description label="Balance">$formatNum($balance)</description>
</hbox>
```

---

### `link` — Hyperlink

Renders a clickable hyperlink with a label. The content is the display text, and the `url` attribute sets the target.

Full reference →

```xml
<link label="Customer Portal" url="./remotecall/portal/$ID">Open Portal</link>
<link label="Documentation" url="https://docs.example.com">View Docs</link>
```

---

### `image` — Image

Renders an image with optional `label`, `caption`, and clickable `url`. The content provides the image source path or URL.

Full reference →

---

### `rule` — Horizontal rule

Renders a horizontal divider line between form sections. Accepts an optional `width` attribute.

Full reference →

```xml
<title>General</title>
<textbox id="name" label="Name" width="300" />

<rule />

<title>Details</title>
<textbox id="description" label="Description" type="multi" height="150" />
```

---

## Input controls

### `textbox` — Text input

The most versatile form control. Renders an editable input whose behavior changes based on the `type` attribute — from single-line text to a rich HTML editor, from a date picker to a currency input. Key attributes: `id`, `label`, `type`, `width`, `placeholder`, `required`, `readonly`, `disabled`, `caption`, `pattern`.

Full reference →

#### Input types

| Type | Description | Extra attributes |
|---|---|---|
| `single` | Single-line text input | `caption`, `pattern` |
| `multi` | Multi-line text area | `height` |
| `html` | Rich HTML editor | `height` |
| `email` | Email address input with validation | `caption`, `pattern` |
| `url` | URL input with validation | `caption`, `pattern` |
| `tel` | Telephone number input | `caption`, `pattern` |
| `password` | Password input (masked characters) | `caption`, `pattern` |
| `num` | Numeric input | `caption` |
| `money` | Monetary value input | `caption` |
| `price` | Price input (general) | `caption` |
| `pricebilling` | Price input (billing context) | `caption` |
| `priceprocurement` | Price input (procurement context) | `caption` |
| `priceproduction` | Price input (production context) | `caption` |
| `country` | Country code selector (ISO 3166-1 alpha-2) | `caption` |
| `currency` | Currency code selector (ISO 4217) | `caption` |
| `unit` | Unit code selector (UN/CEFACT Recommendation 20) | `caption` |
| `date` | Date picker | `caption` |
| `datetime` | Date and time picker | `caption` |

#### Example — Contact form with mixed types

```xml
<title>Contact Details</title>
<hbox>
    <vbox width="50%">
        <textbox id="name" label="Full Name" width="300" required="true" />
        <textbox id="email" label="Email" type="email" width="300" required="true" />
        <textbox id="phone" label="Phone" type="tel" width="300" />
    </vbox>
    <vbox width="50%">
        <textbox id="website" label="Website" type="url" width="300" />
        <textbox id="country" label="Country" type="country" width="200" />
        <textbox id="revenue" label="Annual Revenue" type="money" width="200" caption="€" />
    </vbox>
</hbox>

<rule />

<textbox id="notes" label="Notes" type="multi" height="200" />
```

#### Example — Date range filter

```xml
<hbox>
    <textbox id="date_from" label="From" type="date" width="200" />
    <textbox id="date_to" label="To" type="date" width="200" />
</hbox>
```

#### Example — Numeric inputs with loop

```xml
<for var="quarter" from="1" to="4">
    <hbox>
        <textbox id="revenue_q$quarter$" type="money" width="200"
                 label="Revenue Q$quarter" caption="€" />
        <textbox id="margin_q$quarter$" type="num" width="100"
                 label="Margin Q$quarter" caption="%" />
    </hbox>
</for>
```

#### Example — Readonly computed field

```xml
<textbox id="total" label="Total" type="num" width="200"
         readonly="true">$($subtotal * (1 + $taxrate / 100))</textbox>
```

---

### `checkbox` — Check box

Renders a two-state toggle. The content defines the value submitted when checked. When unchecked, the field is absent from `EXTDATA` (test with `<if value1="$EXTDATA.myCheckbox" func="!=">`). Key attributes: `id`, `label`, `caption`, `selected`, `disabled`.

Full reference →

#### Example — Simple option

```xml
<checkbox id="newsletter" label="Subscribe" caption="Receive newsletter">1</checkbox>
```

#### Example — Dynamic checkbox group

A common pattern is generating checkboxes from data, using unique IDs:

```xml
<title>Services</title>
<foreach var="services" var_value="service">
    <replace pattern="[^A-Za-z0-9]" var="safeId">$service</replace>
    <checkbox id="svc_$safeId$" caption="$service">$service</checkbox>
</foreach>
```

#### Example — Approval workflow

```xml
<checkbox id="approved" caption="I approve this document"
          disabled="$readonly">$session.name||$DATENOW</checkbox>
```

When checked, the value stored is a composite string like `"John Doe||2026-02-08"`, recording both who approved and when.

---

### `listbox` — Selection list

Renders a dropdown (`type="single"`) or a scrollable checkbox list (`type="multi"`). Items are defined with `<listitem>` children. For multi-select, selected values are joined with the `delimiter` (default `", "`) into a single string — use `<split>` or `<decode:csv>` to parse them back. Key attributes: `id`, `label`, `type`, `width`, `required`, `disabled`, `caption` (placeholder for single), `height` (for multi), `delimiter` (for multi).

Full reference →

### `listitem` — List item

Defines a single option within a `<listbox>`. The content is the submitted value; the `caption` attribute is the display text (defaults to the value). Use `selected="true"` to pre-select.

Full reference →

#### Example — Static dropdown

```xml
<listbox id="priority" label="Priority" width="200">
    <listitem caption="Low">1</listitem>
    <listitem caption="Medium" selected="true">2</listitem>
    <listitem caption="High">3</listitem>
    <listitem caption="Critical">4</listitem>
</listbox>
```

#### Example — Dynamic dropdown from database

```xml
<db:select var_result="users" type="assoc">
    <db:fields>
        <db:field>name</db:field>
        <db:field>firstname</db:field>
        <db:field>lastname</db:field>
    </db:fields>
    <db:table>users</db:table>
</db:select>

<listbox id="assignee" label="Assignee" width="300" caption="Select a user...">
    <foreach var="users" var_value="user">
        <listitem caption="$user.firstname $user.lastname">$user.name</listitem>
    </foreach>
</listbox>
```

#### Example — Multi-select with height

```xml
<listbox id="categories" type="multi" label="Categories"
         height="200" delimiter=",">
    <foreach var="settings.categories" var_value="cat">
        <listitem>$cat</listitem>
    </foreach>
</listbox>
```

#### Example — Percentage selector with loop

```xml
<listbox id="probability" label="Probability" width="150">
    <for var="pct" from="0" to="100" step="10">
        <listitem caption="$pct %">$($pct / 100)</listitem>
    </for>
</listbox>
```

---

### `entitybox` — Entity selector

A specialized selector for choosing a ZeyOS entity record (contact, account, ticket, etc.) by ID, with auto-complete search. Use the `tag` attribute to filter results. Key attributes: `id`, `entity`, `tag`, `label`, `caption`, `placeholder`, `width`, `required`, `disabled`.

Full reference →

#### Example — Contact and account selectors

```xml
<hbox>
    <entitybox id="contact_id" entity="contacts" label="Contact"
               width="300" required="true" />
    <entitybox id="account_id" entity="accounts" label="Company"
               width="300" placeholder="Search company..." />
</hbox>
```

#### Example — Filtered entity selection

```xml
<entitybox entity="contacts" id="vendor_id" tag="Vendor"
           label="Vendor" width="300" required="true" />
```

Only contacts tagged "Vendor" appear in the search results.

#### Example — Dynamic entity type

```xml
<listbox id="entityType" label="Entity Type" width="200">
    <listitem caption="Projects">projects</listitem>
    <listitem caption="Tickets">tickets</listitem>
    <listitem caption="Transactions">transactions</listitem>
</listbox>

<if value1="$EXTDATA.entityType" func="!=">
    <entitybox entity="$EXTDATA.entityType" id="entityIndex"
               label="Record" width="300" />
</if>
```

---

## Common layout patterns

### Two-column form

```xml
<hbox>
    <vbox width="50%">
        <textbox id="firstname" label="First Name" width="280" />
        <textbox id="email" label="Email" type="email" width="280" />
        <textbox id="phone" label="Phone" type="tel" width="280" />
    </vbox>
    <vbox width="50%">
        <textbox id="lastname" label="Last Name" width="280" />
        <textbox id="company" label="Company" width="280" />
        <textbox id="position" label="Position" width="280" />
    </vbox>
</hbox>
```

### Sectioned form with dividers

```xml
<title>General Information</title>
<hbox>
    <textbox id="name" label="Name" width="300" required="true" />
    <listbox id="status" label="Status" width="200">
        <listitem>Active</listitem>
        <listitem>Inactive</listitem>
    </listbox>
</hbox>

<rule />

<title>Financial Details</title>
<hbox>
    <textbox id="budget" label="Budget" type="money" width="200" caption="€" />
    <textbox id="currency" label="Currency" type="currency" width="150" />
</hbox>

<rule />

<title>Notes</title>
<textbox id="notes" label="Internal Notes" type="multi" height="150" />
```

### Conditional form sections

```xml
<if value1="$EXTDATA.approved" func="!=">
    <!-- Already approved — show read-only summary -->
    <description label="Approved by">$EXTDATA.approved_by</description>
    <description label="Approved on">
        <date:format format="d.m.Y">$EXTDATA.approved_date</date:format>
    </description>
<else>
    <!-- Not yet approved — show approval controls -->
    <checkbox id="approve" caption="I approve this request">1</checkbox>
    <textbox id="approve_comment" label="Comment" type="multi" height="100" />
</else>
</if>
```

### Data-driven form generation

Generate form controls dynamically from configuration or database queries:

```xml
<foreach var="APPSETTINGS.customFields" var_key="fieldId" var_value="field">
    <switch value1="$field.type">
        <case value="text">
            <textbox id="cf_$fieldId" label="$field.label" width="300" />
        </case>
        <case value="number">
            <textbox id="cf_$fieldId" label="$field.label" type="num"
                     width="200" caption="$field.unit" />
        </case>
        <case value="select">
            <listbox id="cf_$fieldId" label="$field.label" width="300">
                <foreach var="field.options" var_value="opt">
                    <listitem>$opt</listitem>
                </foreach>
            </listbox>
        </case>
        <case value="bool">
            <checkbox id="cf_$fieldId" caption="$field.label">1</checkbox>
        </case>
    </switch>
</foreach>
```

---

## Complete weblet example

A realistic weblet form for contract management, combining all form elements:

```xml
<title>Contract Details</title>

<hbox>
    <vbox width="50%">
        <entitybox id="contact_id" entity="contacts" label="Contact"
                   width="300" required="true" />
        <entitybox id="account_id" entity="accounts" label="Company"
                   width="300" />
        <listbox id="contract_type" label="Type" width="300" required="true">
            <listitem caption="Service Agreement">service</listitem>
            <listitem caption="Maintenance Contract">maintenance</listitem>
            <listitem caption="Consulting">consulting</listitem>
        </listbox>
    </vbox>
    <vbox width="50%">
        <textbox id="contract_num" label="Contract No." width="300"
                 readonly="true">$EXTDATA.contract_num</textbox>
        <textbox id="start_date" label="Start Date" type="date"
                 width="200" required="true" />
        <textbox id="end_date" label="End Date" type="date" width="200" />
    </vbox>
</hbox>

<rule />

<title>Financial</title>

<hbox>
    <textbox id="value" label="Contract Value" type="money"
             width="200" caption="€" />
    <textbox id="currency" label="Currency" type="currency" width="150" />
    <listbox id="billing_cycle" label="Billing Cycle" width="200">
        <listitem>Monthly</listitem>
        <listitem>Quarterly</listitem>
        <listitem>Annually</listitem>
    </listbox>
</hbox>

<rule />

<title>Services</title>

<listbox id="services" type="multi" label="Included Services"
         height="150" delimiter=",">
    <foreach var="APPSETTINGS.serviceOptions" var_value="svc">
        <listitem>$svc</listitem>
    </foreach>
</listbox>

<rule />

<title>Notes</title>

<textbox id="internal_notes" label="Internal Notes" type="multi"
         height="150" />

<hbox>
    <checkbox id="auto_renew" caption="Auto-renew contract">1</checkbox>
    <checkbox id="notify_expiry" caption="Notify before expiry"
              selected="true">1</checkbox>
</hbox>

<rule />

<if value1="$EXTDATA.approved">
    <description label="Approved by">$EXTDATA.approved_by</description>
    <description label="Date">
        <date:format format="d.m.Y H:i">$EXTDATA.approved_date</date:format>
    </description>
<else>
    <description label="Status">Pending approval</description>
</else>
</if>
```

---

Canonical: https://www.zeyos.com/ixml/ixml-guide/user-interface/weblets

# Weblets

A **weblet** is a UI panel embedded within a ZeyOS entity detail view. When a user opens a contact, ticket, account, or any other entity record, weblets appear as tabs or panels alongside the standard entity fields. They are the primary extension point for adding custom interfaces to ZeyOS.

## How weblets work

Weblets are iXML files stored in the `weblets/` directory of a ZeyOS application. Each weblet is registered in `zeyos.app.json` and linked to one or more entity types. When a user navigates to a matching entity record, ZeyOS executes the weblet's iXML and renders the result inside the entity detail view.

### Context variables

The available variables depend on how the weblet runs. A form weblet is registered on a view ending in `.form` and runs while ZeyOS builds the page. Every other weblet is requested by the browser, usually in a frame.

| Variable | Form weblet | Framed or page weblet |
|----------|-------------|-----------------------|
| `$ID` | The ID of the current entity record | Not set. Read the record from `$REQUEST.id` and [check access](#check-record-access) |
| `$ENTITY` | The entity type (e.g., `contacts`, `tickets`) | Not set |
| `$EXTDATA` | Previously saved form data for this record | Not set |
| `$REQUEST` | Not set | HTTP request parameters |
| `$SELFURL`, `$VIEW` | Not set | This weblet's URL and its registered view |
| `$APPSETTINGS` | Application settings for the current instance | Application settings for the current instance |

New-record dialogs differ from record forms. There is no `FORM`, and normally no record variables, because the record does not exist yet.

See [Execution Contexts](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#weblets) for the full list, the weblet types, and how to test each path. These details are confirmed from the platform source. They have not been run for this page.

### Two approaches

**Form-based weblets** use the [`form` namespace](https://www.zeyos.com/ixml/ixml-guide/user-interface/forms) to build native ZeyOS input controls declaratively. The form engine handles rendering, layout, and data persistence through `EXTDATA`. This is the fastest way to build data-entry interfaces.

**Custom weblets** output raw HTML, CSS, and JavaScript using iXML's `<output>` command. This gives you full control over the UI, including custom visualizations, interactive dashboards, or integration with external JavaScript libraries.

## Custom HTML weblets

A custom weblet outputs HTML directly. You can use any standard web technology — HTML, CSS, JavaScript — and load data from the weblet's own URL.

### Basic structure

```xml
<!-- Check access first. See "Check record access" below. -->
<set var="accountId">$REQUEST.id</set>
<cast var="accountId" type="int" />
<session var="session" />

<db:get entity="accounts" id="$accountId" var="account">
    <db:field>ownergroup</db:field>
</db:get>

<array:keyexists var="session.groups" var_result="allowed">$account.ownergroup</array:keyexists>

<is var="allowed" type="non-true">
    <header status="403">Content-Type: text/plain</header>
    <exit>Access denied</exit>
</is>

<output><![CDATA[
<style>
    .dashboard { padding: 16px; font-family: sans-serif; }
    .metric { display: inline-block; margin: 8px; padding: 16px; background: #f5f5f5; border-radius: 4px; }
    .metric-value { font-size: 24px; font-weight: bold; }
    .metric-label { font-size: 12px; color: #666; }
</style>
]]></output>

<!-- Fetch data for the current entity -->
<db:select var_result="stats" type="self">
    <db:fields>
        <db:expression alias="total">COUNT(*)</db:expression>
        <db:expression alias="revenue">SUM("t"."netamount")</db:expression>
    </db:fields>
    <db:table alias="t">transactions</db:table>
    <db:is field="t.account">$accountId</db:is>
</db:select>

<output><![CDATA[
<div class="dashboard">
    <div class="metric">
        <div class="metric-value">]]>$stats.total<![CDATA[</div>
        <div class="metric-label">Transactions</div>
    </div>
    <div class="metric">
        <div class="metric-value">]]>$stats.revenue<![CDATA[ €</div>
        <div class="metric-label">Total Revenue</div>
    </div>
</div>
]]></output>
```

### Check record access

A weblet's URL is reachable by any signed-in user, regardless of its `view`, unless the application is restricted and the user lacks permission for it. The `view` only decides where the weblet appears. `$REQUEST.id` is a value the user can change, so a weblet that reads a record from it must check access itself.

The example above allows a record only when it is owned by one of the user's groups. `db:get` and `db:select` do not apply this check for you. `session.groups` is keyed by group ID.

ZeyOS has no iXML command that applies the platform's full record rule, which also considers assigned users, creators, public records, and module permissions. This example is deliberately stricter: it denies public records and records assigned to the user. Widen it only for cases you have reviewed, and keep the default as denial. The check's logic was run in a local copy of the iXML engine with stand-in values, not on an instance.

### Calling services from JavaScript

A browser call to the application's `REMOTECALL` service is not authenticated by the session. The browser may send its cookie, but the service's auth step ignores it, so the service sees an anonymous caller and, depending on its checks, runs without a user or rejects the call. Connecting the two needs a [reviewed authentication design](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#calling-your-own-service-from-a-weblet).

For most read-only panels, the weblet can answer its own requests instead. Those requests go through the weblet endpoint, which authenticates the session. `$SELFURL` has no query string, so pass the record `id` explicitly:

```xml
<!-- Run the access check above first, so this branch is covered too. -->
<if value1="$REQUEST.action" value2="data">
    <array var="payload">
        <item key="status">ok</item>
    </array>
    <header>Content-Type: application/json</header>
    <output><encode:json var="payload" /></output>
    <exit />
</if>

<output><![CDATA[
<div id="app">
    <button onclick="loadData()">Refresh</button>
    <pre id="result"></pre>
</div>

<script>
function loadData() {
    fetch('$SELFURL?action=data&id=$accountId', { credentials: 'same-origin' })
        .then(r => r.json())
        .then(data => {
            document.getElementById('result').textContent =
                JSON.stringify(data, null, 2);
        });
}
</script>
]]></output>
```

This example has had static review only.

:::warning
This pattern is for reading only. A cross-site page can trigger a request to a weblet URL, and the browser may attach the session cookie. The platform sets no `SameSite` attribute on that cookie, so the outcome depends on browser defaults. Weblet responses also carry no anti-framing header, so another site can frame them by default (both **source-confirmed**).

- `$REQUEST` dispatch and `GET` handlers must never change data.
- A write needs `POST` plus a verified anti-forgery control. The platform provides none for weblets, so that control is your own security design. It needs the same review as [a token bridge to a service](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts#calling-your-own-service-from-a-weblet).
:::

:::tip
For complex custom UIs, keep the iXML weblet thin and put business logic in `resources/`, where the weblet and any service can share it. Never write settings values such as API keys into the page.
:::

## When to use which approach

| Scenario | Approach |
|----------|----------|
| Data-entry form with standard fields | [Form namespace](https://www.zeyos.com/ixml/ixml-guide/user-interface/forms) |
| Custom dashboard or visualization | Custom HTML weblet |
| Interactive read-only UI with updates | Custom HTML that calls its own weblet URL |
| Simple read-only data display | Either — forms with `readonly` fields or custom HTML |
| Generating downloadable files | [Exports](https://www.zeyos.com/ixml/ixml-guide/user-interface/exports) |

## Related topics

- [Execution Contexts](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts)
- [Minimal App](https://www.zeyos.com/ixml/ixml-guide/development-environment/minimal-app)
- [Troubleshooting](https://www.zeyos.com/ixml/ixml-guide/development-environment/troubleshooting)

---

Canonical: https://www.zeyos.com/sdk

# ZeyOS SDK Documentation

Welcome to the ZeyOS SDK documentation.

## Pages

- [Application Activity Management](https://www.zeyos.com/sdk/activity-management)
- [Activate Application](https://www.zeyos.com/sdk/activity-management/zeysdk-activate)
- [Deactivate Application](https://www.zeyos.com/sdk/activity-management/zeysdk-deactivate)
- [Pre-delete Application](https://www.zeyos.com/sdk/activity-management/zeysdk-predelete)
- [Application Management](https://www.zeyos.com/sdk/application-management)
- [Clone Application](https://www.zeyos.com/sdk/application-management/zeysdk-clone)
- [Configure Application](https://www.zeyos.com/sdk/application-management/zeysdk-configure)
- [Create Application](https://www.zeyos.com/sdk/application-management/zeysdk-create)
- [Delete Application](https://www.zeyos.com/sdk/application-management/zeysdk-delete)
- [Host Commands](https://www.zeyos.com/sdk/application-management/zeysdk-host)
- [Migrate Application](https://www.zeyos.com/sdk/application-management/zeysdk-migrate)
- [Rollout Application](https://www.zeyos.com/sdk/application-management/zeysdk-rollout)
- [Asset Management](https://www.zeyos.com/sdk/asset-management)
- [Add Asset](https://www.zeyos.com/sdk/asset-management/zeysdk-add)
- [Remove Asset](https://www.zeyos.com/sdk/asset-management/zeysdk-remove)
- [Rename Asset](https://www.zeyos.com/sdk/asset-management/zeysdk-rename)
- [Update Asset](https://www.zeyos.com/sdk/asset-management/zeysdk-update)
- [Authentication](https://www.zeyos.com/sdk/authentication)
- [Bundle Management](https://www.zeyos.com/sdk/bundle-management)
- [Create Bundle](https://www.zeyos.com/sdk/bundle-management/zeysdk-bundle-create)
- [Install Bundle](https://www.zeyos.com/sdk/bundle-management/zeysdk-bundle-install)
- [Configuration Files](https://www.zeyos.com/sdk/configuration-files)
- [Debugging and Compilation](https://www.zeyos.com/sdk/debugging)
- [ZeyOS Entities](https://www.zeyos.com/sdk/entities)
- [Platform Management](https://www.zeyos.com/sdk/platform-management)
- [Link Instance](https://www.zeyos.com/sdk/platform-management/zeysdk-link)
- [Unlink Instance](https://www.zeyos.com/sdk/platform-management/zeysdk-unlink)
- [Use Instance](https://www.zeyos.com/sdk/platform-management/zeysdk-use)
- [Show Current Instance](https://www.zeyos.com/sdk/platform-management/zeysdk-whoami)
- [Snapshot Management](https://www.zeyos.com/sdk/snapshot-management)
- [Create Snapshot](https://www.zeyos.com/sdk/snapshot-management/zeysdk-snapshot-create)
- [Delete Snapshot](https://www.zeyos.com/sdk/snapshot-management/zeysdk-snapshot-delete)
- [List Snapshots](https://www.zeyos.com/sdk/snapshot-management/zeysdk-snapshot-list)
- [Restore Snapshot](https://www.zeyos.com/sdk/snapshot-management/zeysdk-snapshot-restore)
- [Synchronization](https://www.zeyos.com/sdk/synchronization)
- [Compare Files (zeysdk compare)](https://www.zeyos.com/sdk/synchronization/zeysdk-compare)
- [Pull Files](https://www.zeyos.com/sdk/synchronization/zeysdk-pull)
- [Push Files](https://www.zeyos.com/sdk/synchronization/zeysdk-push)
- [Recover Sync State](https://www.zeyos.com/sdk/synchronization/zeysdk-recover)
- [Sync Files (zeysdk sync)](https://www.zeyos.com/sdk/synchronization/zeysdk-sync)
- [Watch Files](https://www.zeyos.com/sdk/synchronization/zeysdk-watch)
- [ZeyOS Views Reference](https://www.zeyos.com/sdk/views)

---

Canonical: https://www.zeyos.com/sdk/activity-management

# Application Activity Management

The Activity Management commands allow you to control the operational state of your ZeyOS applications.

Available Commands
------------------

| Command | Description | Usage |
|---------|-------------|-------|
| [`activate`](https://www.zeyos.com/sdk/activity-management/zeysdk-activate) | Enable application | `zeysdk activate` |
| [`deactivate`](https://www.zeyos.com/sdk/activity-management/zeysdk-deactivate) | Disable application | `zeysdk deactivate` |
| [`predelete`](https://www.zeyos.com/sdk/activity-management/zeysdk-predelete) | Mark application for deletion | `zeysdk predelete` |

---

Canonical: https://www.zeyos.com/sdk/activity-management/zeysdk-activate

# Activate Application

The `activate` command enables your ZeyOS application in the current instance, making it accessible.

```bash
zeysdk activate [options]
```

**Options**

| Option | Description |
|--------|-------------|
| `--verbose` | Show detailed output |

**Process**

1. **Validation**
   - Checks for sync.json
   - Validates configuration
   - Verifies authentication

2. **Activation**
   - Sets app activity to active
   - Updates platform state
   - Confirms activation

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Missing sync.json | Configuration not found | Run `zeysdk link` |
| Invalid config | Missing required fields | Check sync.json |
| Auth failed | Invalid token | Re-link application |

**Notes**

- Activation is immediate
- All users regain access
- Previous settings restored
- Use after maintenance or updates

**Best Practices**

- Confirm the active instance with `zeysdk whoami` before activation
- Activate after validating recent changes in a staging instance
- Notify users when re-enabling a previously unavailable app
- Use `zeysdk compare` before activation if remote state may be stale

---

Canonical: https://www.zeyos.com/sdk/activity-management/zeysdk-deactivate

# Deactivate Application

The `deactivate` command disables your ZeyOS application in the current instance while preserving it.

```bash
zeysdk deactivate [options]
```

**Options**

| Option | Description |
|--------|-------------|
| `--verbose` | Show detailed output |

**Process**

1. **Validation**
   - Verifies app directory
   - Checks configuration
   - Validates token

2. **Deactivation**
   - Sets app activity to inactive
   - Updates platform state
   - Confirms deactivation

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Not in app directory | Wrong location | Navigate to app root |
| Config missing | No sync.json found | Run `zeysdk link` |
| Network error | Platform unreachable | Check connectivity |

**Notes**

- App remains installed
- Data is preserved
- Users lose access
- Can be reactivated
- Ideal for maintenance

**Best Practices**

- Deactivate before planned maintenance that affects users
- Confirm the active instance before deactivation
- Communicate downtime before disabling production apps
- Reactivate only after validating the app state

---

Canonical: https://www.zeyos.com/sdk/activity-management/zeysdk-predelete

# Pre-delete Application

The `predelete` command marks your ZeyOS application for future deletion in the current instance, preparing it for removal.

```bash
zeysdk predelete [options]
```

**Options**

| Option | Description |
|--------|-------------|
| `--verbose` | Show detailed output |

**Process**

1. **Validation**
   - Checks app context
   - Verifies configuration
   - Validates permissions

2. **Pre-deletion**
   - Sets app activity to pre-deleted
   - Updates platform state
   - Confirms status change

3. **Follow-up**
   - Leaves the application installed
   - Keeps application data available for recovery workflows
   - Allows reactivation when deletion is cancelled

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Invalid context | Not in app directory | Navigate to app root |
| Missing config | No sync.json | Run `zeysdk link` |
| Permission denied | Insufficient rights | Check credentials |

**Notes**

- Reversible action
- App remains installed
- Users lose access
- Data preserved until final deletion
- Can be reactivated if needed

**Best Practices**

1. Before Pre-deletion
   - Backup important data
   - Notify users
   - Document dependencies

2. After Pre-deletion
   - Verify status
   - Update documentation
   - Plan final removal

---

Canonical: https://www.zeyos.com/sdk/application-management

# Application Management

The Application Management commands help you create, configure, and manage ZeyOS applications.

Available Commands
------------------

| Command | Description | Usage |
|---------|-------------|-------|
| [`create`](https://www.zeyos.com/sdk/application-management/zeysdk-create) | Create new application | `zeysdk create <APP_ID> --name=<name>` |
| [`configure`](https://www.zeyos.com/sdk/application-management/zeysdk-configure) | Configure CLI settings | `zeysdk configure` |
| [`clone`](https://www.zeyos.com/sdk/application-management/zeysdk-clone) | Copy application from instance | `zeysdk clone <INSTANCE_ID> <APP_ID> [TARGET_DIRECTORY]` |
| [`delete`](https://www.zeyos.com/sdk/application-management/zeysdk-delete) | Delete application | `zeysdk delete` |
| [`rollout`](https://www.zeyos.com/sdk/application-management/zeysdk-rollout) | Roll out to instances | `zeysdk rollout` |
| [`migrate`](https://www.zeyos.com/sdk/application-management/zeysdk-migrate) | Migrate application | `zeysdk migrate` |
| [`host`](https://www.zeyos.com/sdk/application-management/zeysdk-host) | Manage instance hosts | `zeysdk host <COMMAND>` |


Application Structure
---------------------

```
my-app/
├── .zeysdk/ # SDK sync state and local backups
│   └── sync.json # Empty until the app is linked
├── assets/
├── resources/
├── services/
├── settings/
│   ├── defaultsettings.json
│   ├── settingscode.ixml
│   └── usersettingscode.ixml
├── weblets/
├── README.md
└── zeyos.app.json # App manifest
```

Linking fills the active `.zeysdk/sync.json`, keeps non-active links as `.zeysdk/sync.<INSTANCE_ID>.json`, and adds an instance-specific settings file under `settings`.

---

Canonical: https://www.zeyos.com/sdk/application-management/zeysdk-clone

# Clone Application

The `clone` command copies an existing ZeyOS application from a remote instance.

```bash
zeysdk clone <INSTANCE_ID> <APP_ID> [TARGET_DIRECTORY] [options]
```

**Parameters**

| Parameter | Description | Required | Default | Example |
|-----------|-------------|----------|---------|---------|
| `INSTANCE_ID` | Source instance ID | Yes | - | `dev-instance` |
| `APP_ID` | Application ID | Yes | - | `my-app` |
| `TARGET_DIRECTORY` | Destination path | No | Same as APP_ID | `apps/my-app` |

**Options**

| Option | Description |
|--------|-------------|
| `--verbose` | Show detailed output |
| `--token <token>` | SDK token for non-interactive linking |
| `--url <url>` | Instance URL for non-interactive linking |

**Process**

1. **Directory Setup**
   - If `TARGET_DIRECTORY` not provided, uses `APP_ID` as directory name
   - Requires the target's parent directory to exist
   - Refuses to overwrite any existing target path
   - Creates a uniquely named staging directory beside the final target

2. **App Creation**
   - Creates the local scaffold in the staging directory
   - Sets up initial scaffolding

3. **Instance Linking**
   - Links to specified instance
   - Validates credentials and connection
   - Requires `APP_ID` to already exist remotely; clone will not create it

4. **Data Sync**
   - Performs force pull to sync all app data
   - Retrieves complete application state
   - Renames the completed staging directory to `TARGET_DIRECTORY`
   - Removes staging data on any create, link, pull, or finalization failure

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Directory exists | Target directory already exists | Choose different target directory |
| Parent missing | Parent of the target path does not exist | Create the parent directory first |
| Creation failed | App creation error | Check app identifier and permissions |
| Link failed | Instance linking error | Verify instance ID and connectivity |
| Remote app missing | `APP_ID` does not exist on the source instance | Verify the app ID and source instance |
| Pull failed | Data sync error | Check network and try again |

**Notes**

- `TARGET_DIRECTORY` defaults to `APP_ID`
- The target directory must not already exist
- The target appears only after create, link, and pull have all succeeded
- For a fully non-interactive clone, pass both `--token` and `--url` unless a matching stored host supplies the missing value
- A failure after authentication removes the local staging directory, but a credential saved by the successful link step can remain in the global host store
- The resulting project is linked to the source instance

**Best Practices**

- Clone into a clean workspace
- Use a descriptive target directory when cloning multiple environments
- Run `zeysdk compare` after cloning if you need to verify local and remote state
- Avoid editing files during the initial clone and pull sequence

---

Canonical: https://www.zeyos.com/sdk/application-management/zeysdk-configure

# Configure Application

The `configure` command configures global CLI settings and preferences interactively.

```bash
zeysdk configure
```

**Available Settings**

| Setting | Description | Default |
|---------|-------------|---------|
| `Default Diff Tool` | Choose comparison tool for file diffs | Platform dependent |
| `List Height` | Number of items shown in selection lists | 10 |
| `Run Output Style` | Default output format for `zeysdk run` (`Pretty` or `Simple`) | Pretty |
| `Credential Storage` | Select credential backend (`Auto`, `OS Keyring`, `File Store`) | Auto |

**Process**

1. **Settings Prompt**
   - Opens interactive prompts for global CLI preferences
   - Shows platform-specific diff tool choices
   - Validates numeric settings such as list height
   - Press Escape in a setting screen to return to the settings menu without saving that selection
   - Press Escape or choose Back in the settings menu to exit without saving
   - Returning from a setting keeps that row selected

2. **Credential Backend Selection**
   - Lets you choose automatic storage, OS keyring, or file store
   - Uses the selected backend for future saved credentials
   - Persists the preference for later commands

3. **Save Configuration**
   - Stores the updated settings
   - Applies preferences to subsequent CLI runs
   - Uses environment overrides when present

**Diff Tools by Platform**

1. **Windows**
   - Visual Studio Code
   - Beyond Compare
   - Hunk
   - WinMerge
   - Araxis Merge
   - DiffMerge
   - TortoiseMerge
   - P4Merge
   - Other (prompts for a custom command when comparison starts)
   - None

2. **macOS**
   - Visual Studio Code
   - Beyond Compare
   - Hunk
   - FileMerge
   - Araxis Merge
   - DiffMerge
   - Kaleidoscope
   - P4Merge
   - Other (prompts for a custom command when comparison starts)
   - None

3. **Linux**
   - Visual Studio Code
   - Hunk
   - Meld
   - KDiff3
   - Kompare
   - Diffuse
   - xxdiff
   - P4Merge
   - Other (prompts for a custom command when comparison starts)
   - None

**Diff Tool Setup**

- Install the selected tool separately and make its command available on `PATH`.
- Beyond Compare uses `bcomp` on macOS and `BComp.com` on Windows. These launchers wait until the comparison closes, keeping the remote temporary file available. On macOS, use Beyond Compare's **Install Command Line Tools** menu item if `bcomp` is unavailable. See the [Beyond Compare command-line reference](https://www.scootersoftware.com/v5help/command_line_reference.html).
- [Hunk](https://www.hunk.dev) is available on all three platforms. Once its `hunk` command is installed, select **Default Diff Tool → Hunk**. ZeySDK runs `hunk diff --files` in the current terminal and waits for it to exit. See [Hunk's file comparison guide](https://www.hunk.dev/docs/workflows/files-and-patches/).
- Hunk provides a diff viewer. During a sync conflict, edit the local file in your editor before confirming that the merge is finished.
- Custom commands should wait for the comparison to finish before exiting; add the tool's wait flag where available.

**Configuration Storage**

General CLI settings are saved through the credential backend. The credential-storage preference itself is saved in `~/.config/zeysdk/settings.json`.

- `Auto`: Tries the OS keyring on supported desktop systems and falls back to the local file store; WSL and headless Linux select the file store directly
- `OS Keyring`: Uses platform keychain when available
- `File Store`: Stores credentials unencrypted in `~/.config/zeysdk/store.json` with owner-only file permissions

Environment variables (`ZEYSDK_USE_KEYRING` or legacy `ZEYSDK_USE_KEYTAR`) override configured storage mode.

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Storage access | Failed to access configured credential backend | Check keychain availability or switch to File Store |
| Invalid saved configuration | Saved data is malformed JSON or is not a JSON object | Correct the saved value through the configured credential backend; the command leaves it unchanged |
| Invalid input | Invalid list height value | Enter a positive number |
| Save failed | Failed to save configuration | Verify write permissions |

**Notes**

- Configuration is global, not per application
- Diff tool selection affects compare and merge workflows
- `File Store` is useful in environments without OS keyring support
- Selecting `OS Keyring` explicitly fails if keyring access is unavailable; `Auto` may fall back
- Environment variables take precedence over saved credential storage settings

**Best Practices**

- Prefer `Auto` credential storage on developer workstations
- Use a diff tool that can be launched from the command line
- Keep list height high enough for projects with many instances or snapshots
- Use file store only when keyring access is unavailable or unsuitable

---

Canonical: https://www.zeyos.com/sdk/application-management/zeysdk-create

# Create Application

The `create` command creates a local ZeyOS application by setting up the application manifest and default directory structure.

```bash
zeysdk create <APP_ID> [options]
```

**Parameters**

| Parameter | Description | Required | Example |
|-----------|-------------|----------|---------|
| `APP_ID` | Application ID | Yes | `my-app` |

**Options**

| Option | Description | Required | Example |
|--------|-------------|----------|---------|
| `--name <name>` | Display name; prompted when omitted | No | `--name="My App"` |
| `--vendor <vendor>` | Vendor name | No | `--vendor="ACME Inc"` |
| `--verbose` | Show detailed output | No | `--verbose` |

**Process**

1. **Input Validation**
   - Requires an application ID
   - Uses the display name from `--name` or prompts for one
   - Uses the vendor name when provided

2. **Project Scaffolding**
   - Refuses to run inside an existing app tree
   - Refuses to overwrite any existing target path
   - Creates the application directory named by `APP_ID`
   - Creates standard asset and configuration folders
   - Writes the initial `zeyos.app.json` manifest

3. **Initial Files**
   - Creates default settings files
   - Creates an empty `.zeysdk/sync.json`
   - Creates an empty project `README.md`
   - Leaves the app ready for asset management and linking

Creates a new ZeyOS application with the following structure:

```
my-app/
|-- .zeysdk/
|   `-- sync.json
|-- assets/
|-- resources/
|-- services/
|-- settings/
|   |-- defaultsettings.json
|   |-- settingscode.ixml
|   `-- usersettingscode.ixml
|-- weblets/
|-- README.md
`-- zeyos.app.json
```

**Examples**

```bash
zeysdk create my-app --name="My App"
zeysdk create customer-tools --name="Customer Tools" --vendor="ACME Inc"
zeysdk create reporting --name="Reporting" --verbose
```

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Missing app ID | Required application ID was not provided | Pass `APP_ID` |
| Missing name | Display name was not provided | Pass `--name` or enter one when prompted |
| Invalid app ID | ID has unsupported characters or punctuation at either end | Use letters, numbers, dots, underscores, and hyphens, beginning and ending with a letter or number |
| Nested app | Command was run inside another app | Move to a parent workspace directory |
| Directory exists | Target directory already exists | Choose another `APP_ID` or directory |
| Write error | Files could not be created | Check local permissions |

**Notes**

- `create` only creates the local project structure
- If scaffolding fails after the directory is created, the incomplete target is removed
- Use `zeysdk link` to connect the app to an instance
- Use `zeysdk add` to register new assets after creation
- Keep `APP_ID` stable once the app is shared or deployed

**Best Practices**

- Use lowercase, descriptive application IDs
- Set `--vendor` when the app belongs to a company or team
- Commit the generated project before making large changes
- Link and push only after reviewing the generated manifest

---

Canonical: https://www.zeyos.com/sdk/application-management/zeysdk-delete

# Delete Application

The `delete` command permanently deletes your ZeyOS application from the current instance.

```bash
zeysdk delete [options]
```

**Options**

| Option | Description |
|--------|-------------|
| `-f, --force` | Skip deletion confirmation prompt |
| `--verbose` | Show detailed output |

**Process**

1. **Validation**
   - Checks that the command runs inside a linked app
   - Loads `.zeysdk/sync.json`
   - Verifies required instance and app identifiers

2. **Confirmation**
   - Prompts before deleting the remote application
   - Stops when deletion is not confirmed
   - Uses verbose output for additional progress details

3. **Deletion**
   - Sends the delete request to the current instance
   - Removes the application from the platform
   - Reports the final deletion result

**Examples**

```bash
zeysdk delete
zeysdk delete --force
zeysdk delete --verbose
```

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Missing sync.json | sync.json file not found | Ensure you are in an app directory |
| Invalid sync.json | Missing required keys | Verify sync.json has id and url |
| API error | Server communication failed | Check connection and permissions |

**Warning**

Delete permanently removes the application from the current instance.

**Notes**

- Delete acts on the currently linked instance
- Local files are not a substitute for a remote backup
- Use `zeysdk whoami` before deleting if you are unsure which instance is active
- Consider `zeysdk predelete` when you need a reversible operational state first
- `--force` skips confirmation and should be used carefully

**Best Practices**

- Create a snapshot before destructive changes
- Confirm the active instance before deleting
- Notify affected users before removing production apps
- Keep a record of the app ID and instance for audit purposes

---

Canonical: https://www.zeyos.com/sdk/application-management/zeysdk-host

# Host Commands

The `host` command manages saved ZeyOS instance hosts.

```bash
zeysdk host <COMMAND> [options]
```

**Available Commands**

| Command | Description |
|---------|-------------|
| `list` | List all configured hosts |
| `add [HOST_NAME]` | Add a new host |
| `update [HOST_NAME]` | Update existing host |
| `remove [HOST_NAME]` | Remove host configuration |
| `link [HOST_NAME]` | Link current app to host |

**Options for `add` and `update`**

| Option | Description |
|--------|-------------|
| `-u, --url <url>` | URL for the host |
| `-t, --token <token>` | Authentication token |

**Process**

1. **Host Listing**
   - Reads saved host credentials
   - Displays configured host names and custom URLs; standard `cloud.zeyos.com` URLs are omitted from the compact listing
   - Hides stored tokens from output

2. **Host Creation and Updates**
   - Accepts host name, URL, and token from flags or prompts
   - Validates required values
   - Normalizes URLs by removing trailing slashes
   - Refuses to add a second entry with the same host name
   - Preserves the current URL or token when only the other field is supplied to `update`
   - Keeps the exact saved URL selected in interactive update, removal, and linking prompts
   - Rejects ambiguous positional host names before changes; omit the host name to choose the exact URL
   - Refuses to move an entry onto another saved credential for the same host and URL
   - Writes an updated URL-keyed credential before removing the old key; if old-key cleanup fails, it attempts to remove the replacement and keeps the known-good entry

3. **Host Removal and Linking**
   - Always asks for confirmation before removing saved credentials
   - Links the current app to a saved host when using `host link`
   - Requires app context for app-linking operations

**Examples**

```bash
# List all hosts
zeysdk host list
# Output: Configured hosts:
#         - instance1
#         - custom     https://zeyos.example.com/custom

# Add new host with options
zeysdk host add myinstance -u https://cloud.zeyos.com/app -t mytoken

# Add new host interactively
zeysdk host add
# Output: Enter the host name: myinstance
#         Enter the URL: https://cloud.zeyos.com/app
#         Enter the token: ****

# Update host with options
zeysdk host update myinstance --url https://cloud.zeyos.com/newapp
zeysdk host update myinstance --token replacement-token

# Update host interactively
zeysdk host update
# Output: Select a host to update:
#         > instance1
#         > instance2

# Remove host
zeysdk host remove myinstance

# Link the current app with a saved host
zeysdk host link myinstance
```

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Host exists | Duplicate host | Update existing host |
| Not in app | Outside app directory | Navigate to app directory for link command |
| Write error | Cannot save config | Check permissions |
| Host not found | Invalid host name | Check host name |
| Multiple saved URLs | Host name matches more than one saved endpoint | Omit the host name and select the exact URL interactively |
| Destination exists | Updating a URL would replace another saved credential | Select that destination entry to update its token |
| Link/auth error | Saved URL or token cannot link the app | Update the host and retry `host link` |

**Notes**

- The `link` command is only available when inside a ZeyOS app directory
- `host add` and `host update` save values without making a network request; `host link` performs connection validation
- `host add` defaults a missing URL to `https://cloud.zeyos.com/<HOST_NAME>`; `host link` follows normal link behavior and can create the remote app when it is missing
- `host remove` has no force or `--yes` option
- Storage follows the configured credential backend: the OS keyring when selected and available, or the unencrypted local file store

**Best Practices**

- Use clear host names such as `dev`, `staging`, or `prod`
- Keep production tokens separate from development tokens
- Rotate tokens when team access changes
- Verify the host URL before linking an application

---

Canonical: https://www.zeyos.com/sdk/application-management/zeysdk-migrate

# Migrate Application

The `migrate` command converts recognized legacy SDK metadata and instance sync files in the current directory to the current layout.

```bash
zeysdk migrate [options]
```

**Options**

| Option | Description |
|--------|-------------|
| `--link` | Link application after migration |
| `--verbose` | Show detailed output |

**Directory Structure Migration**

The command recognizes these legacy files:

- `.zeysdk.assets.json`
- `.zeysdk.package.json`
- `.zeysdk.sync.<INSTANCE_ID>.json`, where the instance ID is a safe SDK identifier

It updates them toward this structure:

```
my-app/
|-- .zeysdk/
|   |-- sync.json                 # Active link, created later by link/use
|   `-- sync.<INSTANCE_ID>.json   # Migrated non-active instance state
|-- assets/
|-- resources/
|-- services/
|-- settings/
|-- weblets/
`-- zeyos.app.json
```

**Process**

1. **Pre-migration Validation**
   - Requires at least one recognized legacy package, assets, or instance sync file
   - Requires each JSON metadata file, including an existing `zeyos.app.json`, to contain a JSON object
   - Requires the merged manifest to contain `identifier` and `name`
   - Refuses to overwrite an existing `.zeysdk/sync.<INSTANCE_ID>.json` destination

2. **Configuration Migration**
   - Merges existing `zeyos.app.json`, then `.zeysdk.assets.json`, then `.zeysdk.package.json`
   - Later sources win when the same property appears more than once
   - Writes the merged result atomically to `zeyos.app.json`

3. **Directory Migration**
   - Creates `.zeysdk` when needed
   - Moves `.zeysdk.sync.<INSTANCE_ID>.json` to `.zeysdk/sync.<INSTANCE_ID>.json`
   - Removes the legacy package and assets files after the new manifest succeeds
   - Preserves existing asset directories and files

4. **Rollback and Optional Linking**
   - Stages legacy metadata files and records moved sync files while migration is in progress
   - Restores moved files and the previous manifest if migration fails
   - Runs the normal interactive `zeysdk link` flow only after migration succeeds when `--link` is used
   - Does not roll the completed file migration back if that later link step fails

**Examples**

```bash
zeysdk migrate
zeysdk migrate --link
zeysdk migrate --verbose
```

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| No legacy inputs | No recognized legacy files were found | Run from the legacy app root and verify filenames |
| Invalid metadata | A JSON file is not an object or merged metadata lacks `identifier`/`name` | Repair the legacy metadata before retrying |
| Sync conflict | A destination sync file already exists | Reconcile the old and current sync files manually |
| Permission denied | Cannot modify files | Check file permissions |
| Migration conflict | Incompatible changes | Resolve conflicts manually |
| Link failed | Post-migration link failed | Check instance credentials |

**Notes**

- Migration does not move credentials or create an active `.zeysdk/sync.json`; use `--link`, `zeysdk link`, or `zeysdk use` as appropriate afterward
- Successful migration removes the recognized legacy metadata files; a cleanup failure can leave a `.migrating-<id>` file and emits a warning
- `--link` validates and saves an instance connection after file migration
- `--verbose` shows detailed progress information

**Best Practices**

- Commit or manually copy the legacy hidden files before migration; the normal snapshot glob does not include root files such as `.zeysdk.assets.json`
- Review generated `zeyos.app.json` before pushing changes
- Run `zeysdk compare` after migration to inspect local and remote differences
- Commit the migrated structure separately from feature changes

---

Canonical: https://www.zeyos.com/sdk/application-management/zeysdk-rollout

# Rollout Application

The `rollout` command pushes your ZeyOS application to one or more linked instances sequentially.

```bash
zeysdk rollout [INSTANCE_ID...] [options]
```

**Parameters**

| Parameter | Description | Required | Example |
|-----------|-------------|----------|---------|
| `INSTANCE_ID` | Target instance IDs | No | `dev-instance prod-instance` |

**Options**

| Option | Description |
|--------|-------------|
| `-a, --all` | Rollout to all linked instances |
| `-f, --force` | Continue even if errors occur |
| `-v, --verbose` | Show detailed output |
| `--break-lock` | Remove an existing sync lock before acquiring the rollout lock |

**Interactive Features**

1. **Instance Selection**
   - If no instances specified, shows interactive checkbox list
   - Currently active instance pre-selected
   - Must select at least one instance

2. **Backup Prompt**
   - Option to create server state backup before rollout
   - Defaults to creating a backup
   - Copies the project once per valid target, switches the copy to that instance, and force-pulls its server state
   - Creates `.zeysdk/backups/server-state_<TIMESTAMP>.zip`
   - Excludes `node_modules`, `.git`, `dist`, `build`, nested backups, and snapshots from copied backup state

**Process**

1. **Validation**
   - Verifies `.zeysdk` directory exists
   - Validates instance tokens
   - Checks app identifier in `zeyos.app.json`

2. **Execution Order**
   - Active instance processed first (if included)
   - Remaining instances processed sequentially
   - Acquires one project sync lock for the rollout and passes that ownership through every internal force-push
   - Automatically switches between instances
   - Returns to original instance when complete

3. **Error Handling**
   - Stops on first error unless `--force` used
   - Creates backups before acquiring the rollout lock and before making remote changes when requested
   - Attempts to restore the original *active-instance selection* after success or failure; it does not roll back remote pushes that already succeeded
   - Continues with valid instances if token validation fails with `--force`
   - Records forced validation, backup, and rollout failures and exits nonzero after all eligible instances have been processed
   - Still aborts when the combined backup ZIP itself cannot be created

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| No instances | No valid target instances found | Specify instances or use `--all` |
| Token invalid | Instance authentication failed | Check instance credentials |
| Backup failed | Unable to create backup | Verify write permissions |
| Rollout failed | Error during deployment | Check error message or use `--force` |
| Switch failed | Unable to change instance | Manually switch using `use` command |
| Lock busy | Another sync operation owns the project lock | Wait for it to finish; use `--break-lock` only after confirming the owner is stale |

**Notes**

- `rollout` can target explicit instance IDs, selected instances, or all linked instances
- The active instance is pre-selected during interactive rollout
- The command attempts to return to the original instance when rollout completes; failure to restore it makes the command fail
- Pushes share one rollout-owned lock and run one at a time; rollout does not push to instances concurrently
- `--force` continues across eligible instances when possible, but any recorded partial failure still makes the overall command fail with a nonzero exit
- A target that fails credential validation is skipped under `--force` and included in the final failure summary
- `--break-lock` is destructive to lock coordination and must not be used while another live SDK operation is working in the same project

**Best Practices**

- Run `zeysdk compare` before rolling out to important instances
- Create backups before broad rollouts
- Keep the generated server-state ZIP until the rollout has been verified
- Roll out to staging before production
- Prefer explicit instance IDs for release scripts
- Review failures before retrying with `--force`

---

Canonical: https://www.zeyos.com/sdk/asset-management

# Asset Management

The `add` command creates local files, while all Asset Management commands maintain their entries in `zeyos.app.json`. Supported classes are `services`, `resources`, `weblets`, and generic `assets`; only generic assets may use nested paths.

Available Commands
------------------

| Command | Description | Usage |
|---------|-------------|-------|
| [`add`](https://www.zeyos.com/sdk/asset-management/zeysdk-add) | Add new asset | `zeysdk add <ASSET_PATH>` |
| [`update`](https://www.zeyos.com/sdk/asset-management/zeysdk-update) | Update asset metadata | `zeysdk update <ASSET_PATH>` |
| [`remove`](https://www.zeyos.com/sdk/asset-management/zeysdk-remove) | Remove asset | `zeysdk remove <ASSET_PATH>` |
| [`rename`](https://www.zeyos.com/sdk/asset-management/zeysdk-rename) | Rename asset | `zeysdk rename <OLD_ASSET_PATH> <NEW_ASSET_PATH>` |

---

Canonical: https://www.zeyos.com/sdk/asset-management/zeysdk-add

# Add Asset

The `add` command creates a local asset file and registers it in `zeyos.app.json`.

```bash
zeysdk add <ASSET_PATH> [options]
```

**Parameters**

| Parameter | Description | Required | Example |
|-----------|-------------|----------| ------- |
| `ASSET_PATH` | Asset path including class directory and filename | Yes | `services/my-service.ixml` |

**Options**

| Option | Description |
|--------|-------------|
| `--verbose` | Show detailed output |
| `--name <name>` | Asset name |
| `--type <type>` | Service or weblet type; required with non-interactive service/weblet usage |
| `--view <view>` | Weblet view |
| `--entity <entity>` | Entity for an entity-triggered service |
| `--schedule <minutes>` | `TIMING` start time as an integer from `0` to `1439` |
| `--interval <minutes>` | `TIMING` interval as an integer of at least `1` |
| `--public <value>` | Resource public flag (`0` or `1`) |
| `--url <url>` | Empty or an `http://`/`https://` URL for a weblet or service |
| `--accesskey <key>` | HTTP access key for `REMOTECALL` services |
| `--width <pixels>` | Popup or embedded weblet width; non-negative integer |
| `--height <pixels>` | Popup or embedded weblet height; non-negative integer |
| `--svgpath <path>` | SVG path for an `INTEGRATED`, `STANDALONE`, or `DETACHED` icon |
| `--color <hex>` | Three or six uppercase hex digits without `#`, such as `FF0000` |
| `--langaliases <json>` | Language aliases as valid JSON text |

Any metadata flag selects non-interactive mode. In that mode, services and weblets must include `--type`; type names are case-insensitive and stored in uppercase. With no metadata flags, the command prompts for applicable fields. Generic `assets` never prompt for metadata.

**Supported Types**

- Services: `TIMING`, `REMOTECALL`, `AFTER_CREATION`, `BEFORE_MODIFICATION`, `AFTER_MODIFICATION`, `AFTER_CREATION_MODIFICATION`, `BEFORE_DELETION`, `AFTER_DELETION`
- Weblets: `INTEGRATED`, `STANDALONE`, `DETACHED`, `POPUP_FRAMED`, `POPUP_PLAIN`, `EMBEDDED_FRAMED`, `EMBEDDED_COLLAPSED`, `EMBEDDED_PLAIN`

Entity-triggered services require `--entity`. `TIMING` services use `--schedule` and `--interval`; interactive mode asks for the schedule as `HH:MM`. Popup and embedded weblets use width and height. Integrated, standalone, and detached weblets use SVG path and color.

**Behavior**

1. Validates the app, asset class, filename, and manifest key.
2. Rejects absolute paths, traversal, symbolic-link path components, and an existing physical file.
3. Creates missing parent directories and the local file. New `.ixml` files receive the default iXML template; other files start empty.
4. Adds the manifest entry only after file creation succeeds. A later manifest failure removes the newly created file.

`assets` supports nested paths such as `assets/images/logo.png`; services, resources, and weblets use a single filename below their class directory.

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Invalid asset class | Unsupported asset type | Use correct asset class |
| Missing app.json | Configuration file not found | Initialize app first |
| Duplicate asset | Manifest key or physical file already exists | Use a different path or `update` |
| Unsafe filename | Invalid characters, traversal, absolute path, or symbolic link | Keep the path inside its asset directory |
| Missing type/entity | Required non-interactive metadata is absent | Supply the required flag or use interactive mode |
| Invalid metadata | Value fails type, range, URL, color, or JSON validation | Correct the supplied value |

**Notes**

- Asset paths start with `services`, `resources`, `weblets`, or `assets`.
- MIME type is inferred from the filename.
- Use `zeysdk push` or `zeysdk sync` to send the new asset to an instance
- Run the command from the application root.

**Examples**

```bash
# Public resource
zeysdk add resources/logo.png --name "Company Logo" --public 1

# Daily service starting at 02:00
zeysdk add services/daily-backup.ixml \
  --name "Daily Backup" --type TIMING --schedule 120 --interval 1440

# Entity hook
zeysdk add services/contact-hook.ixml \
  --name "Contact Hook" --type AFTER_CREATION --entity contacts

# Popup weblet
zeysdk add weblets/customer-dashboard.ixml \
  --name "Customer Dashboard" --type POPUP_FRAMED \
  --width 800 --height 600 --view customers.index

# Integrated weblet with icon metadata
zeysdk add weblets/quick-note.ixml \
  --name "Quick Note" --type INTEGRATED --view notes.index \
  --color FF0000 --svgpath 'M5 5h90v90h-90z' \
  --langaliases '{"de_DE":"Schnellnotiz"}'

# Nested generic asset (no metadata prompt)
zeysdk add assets/images/icons/favicon.ico
```

---

Canonical: https://www.zeyos.com/sdk/asset-management/zeysdk-remove

# Remove Asset

The `remove` command removes an existing asset from your ZeyOS application.

```bash
zeysdk remove <ASSET_PATH> [options]
```

**Parameters**

| Parameter | Description | Required | Example |
|-----------|-------------|----------| ------- |
| `ASSET_PATH` | Asset path including class directory and filename | Yes | `services/my-service.ixml` |

**Options**

| Option | Description |
|--------|-------------|
| `--verbose` | Show detailed output |

**Process**

1. **Validation**
   - Verifies `zeyos.app.json` exists
   - Validates the asset class from `ASSET_PATH`
   - Rejects traversal, absolute paths, symbolic-link path components, and invalid characters
   - Confirms the asset key exists and its extension matches the MIME type in the manifest

2. **Manifest Update**
   - Removes the asset entry from `zeyos.app.json`
   - Preserves unrelated asset definitions

3. **File Removal**
   - Deletes the local asset file
   - Removes empty parent directories below the asset-class directory
   - Treats an already-missing local file as a successful manifest cleanup
   - Reports progress when verbose output is enabled
   - Leaves remote deletion to `zeysdk push` or `zeysdk sync`

Generic `assets` may use nested paths. Services, resources, and weblets use a single filename below their class directory. The operation is serialized with other local manifest mutations, and a filesystem error restores the manifest entry.

**Error Handling**

| Error | Description | Resolution | Example |
|-------|-------------|------------|---------|
| Invalid asset class | Wrong type specified | Use correct class | `Invalid asset class. Expected: services, resources, weblets, assets` |
| Missing app.json | Config file not found | Initialize app | `requires the zeyos.app.json file to exist` |
| Asset not found | Asset doesn't exist | Check asset name | `requires the asset key to exist in zeyos.app.json` |
| Extension mismatch | Filename does not match the registered MIME type | Use the registered path | `requires the file extension to match` |
| Unsafe path | Path escapes its asset directory or contains a symbolic link | Use a path inside the app | `requires the asset file path to stay inside` |
| File delete error | Cannot remove file | Check permissions | `There was a problem deleting the file` |

**Notes**

- `remove` deletes the local file and its manifest entry
- The command does not prompt for confirmation
- The remote instance is not updated until you push or sync
- Removing an asset can affect services, weblets, or references that depend on it
- Use `zeysdk compare` to review removal before synchronizing

**Best Practices**

- Search for references before removing shared resources
- Create a snapshot before large cleanup work
- Remove unused manifest entries and files together
- Push removals during a planned maintenance window when users may be affected

**Examples**

```bash
# Remove a service
zeysdk remove services/backup.ixml --verbose
# Output:
# Loading assets info.
# Updating assets info.
# Deleting file: services/backup.ixml (...)
# File deleted successfully.
# Remove was completed successfully.

# Remove a weblet
zeysdk remove weblets/dashboard.ixml

# Remove a resource
zeysdk remove resources/logo.png

# Remove a nested generic asset
zeysdk remove assets/images/icons/favicon.ico
```

---

Canonical: https://www.zeyos.com/sdk/asset-management/zeysdk-rename

# Rename Asset

The `rename` command renames an existing asset in your ZeyOS application.

```bash
zeysdk rename <OLD_ASSET_PATH> <NEW_ASSET_PATH> [options]
```

**Parameters**

| Parameter | Description | Required | Example |
|-----------|-------------|----------| ------- |
| `OLD_ASSET_PATH` | Old asset path including class directory and filename | Yes | `services/old-service.ixml` |
| `NEW_ASSET_PATH` | New asset path including class directory and filename | Yes | `services/new-service.ixml` |

**Options**

| Option | Description |
|--------|-------------|
| `--verbose` | Show detailed output |

**Validation Rules**

1. **Source Validation**
   - Asset class must be valid (`services`, `resources`, `weblets`, `assets`)
   - Source key must exist in `zeyos.app.json`
   - Source extension must match the MIME type registered in the manifest
   - Source path must remain inside its asset directory and contain no symbolic-link component

2. **Destination Validation**
   - Asset classes must match between source and destination
   - A different destination key must not exist in the manifest
   - A different destination path must not exist in the filesystem, including as an untracked file or dangling symbolic link
   - New filename must be valid

3. **Configuration**
   - `zeyos.app.json` must exist
   - Asset must be properly configured

**Process**

1. Renames the local file, creating a missing destination parent directory for nested generic assets.
2. Updates the manifest key and MIME type based on the new extension.
3. Records a key-changing rename in `.zeysdk/sync.json` for compare and sync when that file is available.
4. Restores the original file, manifest, and sync metadata if a later step fails.

Generic `assets` may move between nested paths, but both paths must remain under `assets/`. Other asset classes use a single filename. Rename does not update references in source code or other applications.

**Error Handling**

| Error | Description | Resolution | Example Message |
|-------|-------------|------------|-----------------|
| Invalid arguments | Missing parameters | Provide both paths | `expects at least 2 arguments` |
| Asset class mismatch | Different classes | Use same class | `expects the asset class directory to be the same` |
| Invalid asset class | Wrong type | Use valid class | `should be a valid asset class: services, resources, weblets, assets` |
| Missing config | No app.json | Initialize app | `requires the zeyos.app.json file to exist` |
| Source missing | Source not found | Check source path | `requires the asset to exist` |
| Source extension mismatch | Source extension differs from registered MIME type | Use the registered filename | `requires the source extension to match` |
| Unsafe path | Path escapes its class directory or contains a symbolic link | Use an in-app path | `requires asset file paths to stay inside` |
| Destination exists | Tracked or untracked target exists | Choose a different name | `requires the destination file to not exist` |
| Rename failed | File system error | Check permissions | `There was a problem renaming the file` |

**Automatic Updates**

The following are automatically handled:

1. MIME type updates based on the new extension
2. Manifest key and file-path updates
3. In-process rollback on failure
4. Rename tracking in `.zeysdk/sync.json` when the manifest key changes

**Notes**

- Source and destination paths must use the same asset class
- Rename updates metadata and the local file path
- Existing destinations are never intentionally overwritten
- Remote state is not updated until you push or sync
- Rename tracking helps compare and sync display the change as a rename when possible

**Best Practices**

- Rename before pushing new references to the asset
- Keep asset keys stable once they are used by other apps or integrations
- Use `zeysdk compare` after renaming to verify the pending operation
- Prefer one rename per asset over rename-plus-content edits in the same change

**Examples**

```bash
# Rename a service
zeysdk rename services/old-backup.ixml services/new-backup.ixml --verbose
# Output:
# Loading assets info.
# Renaming file.
# Updating assets info.
# Rename was completed successfully.

# Rename a weblet (changing extension)
zeysdk rename weblets/dashboard.ixml weblets/dashboard-test.ixml
# Updates mimetype automatically

# Rename a resource
zeysdk rename resources/old-logo.png resources/new-logo.png

# Move a nested generic asset within assets/
zeysdk rename assets/drafts/logo.svg assets/images/logo.svg
```

---

Canonical: https://www.zeyos.com/sdk/asset-management/zeysdk-update

# Update Asset

The `update` command changes an existing manifest entry without modifying the local asset file.

```bash
zeysdk update <ASSET_PATH> [options]
```

**Parameters**

| Parameter | Description | Required | Example |
|-----------|-------------|----------|---------|
| `ASSET_PATH` | Asset path including class directory and filename | Yes | `services/my-service.ixml` |

**Options**

| Option | Description |
|--------|-------------|
| `--verbose` | Show detailed output |
| `--name <name>` | Asset name |
| `--type <type>` | Service or weblet type; case-insensitive |
| `--view <view>` | Weblet view |
| `--entity <entity>` | Entity for an entity-triggered service |
| `--schedule <minutes>` | `TIMING` start time as an integer from `0` to `1439` |
| `--interval <minutes>` | `TIMING` interval as an integer of at least `1` |
| `--public <value>` | Resource public flag (`0` or `1`) |
| `--url <url>` | Empty or an `http://`/`https://` URL for a weblet or service |
| `--accesskey <key>` | HTTP access key for `REMOTECALL` services |
| `--width <pixels>` | Popup or embedded weblet width; non-negative integer |
| `--height <pixels>` | Popup or embedded weblet height; non-negative integer |
| `--svgpath <path>` | SVG path for an `INTEGRATED`, `STANDALONE`, or `DETACHED` icon |
| `--color <hex>` | Three or six uppercase hex digits without `#` |
| `--langaliases <json>` | Language aliases as valid JSON text; an empty value stores `null` |

Any metadata flag selects non-interactive mode and merges applicable values over the existing entry. With no metadata flags, services, resources, and weblets use interactive prompts. Generic `assets` preserve their existing `content` and have no editable metadata prompts.

**Validation and Behavior**

1. Requires `zeyos.app.json` and an existing asset key.
2. Rejects absolute paths, traversal, symbolic-link path components, and invalid characters.
3. For services, resources, and weblets, requires the supplied extension to match the MIME type registered in the manifest. Use `rename` to change an extension.
4. Validates type-specific metadata, then rewrites only the selected manifest entry. The local file contents are not read or changed.

Service and weblet types are the same as for `add`. Type flags are normalized to uppercase. Entity-triggered services require an entity; `--schedule` and `--interval` enforce the documented `TIMING` ranges. Popup and embedded weblets use dimensions, while integrated, standalone, and detached weblets use SVG path and color.

**Service Types**

| Type | Description | Schedule | Interval |
|------|-------------|----------|-----------|
| `TIMING` | Scheduled task | Minutes from midnight (`0`-`1439`) | Integer of at least `1` |
| `REMOTECALL` | External API call | N/A | N/A |
| `AFTER_CREATION` | Post-create hook | N/A | N/A |
| `BEFORE_MODIFICATION` | Pre-update hook | N/A | N/A |
| `AFTER_MODIFICATION` | Post-update hook | N/A | N/A |
| `AFTER_CREATION_MODIFICATION` | Combined hook | N/A | N/A |
| `BEFORE_DELETION` | Pre-delete hook | N/A | N/A |
| `AFTER_DELETION` | Post-delete hook | N/A | N/A |

**Weblet Types**

| Type | Description | Width/Height |
|------|-------------|--------------|
| `INTEGRATED` | In-app weblet | Not used |
| `STANDALONE` | Independent window | Not used |
| `DETACHED` | Floating window | Not used |
| `POPUP_FRAMED` | Modal with frame | Non-negative integers |
| `POPUP_PLAIN` | Modal without title | Non-negative integers |
| `EMBEDDED_FRAMED` | Inline with frame | Non-negative integers |
| `EMBEDDED_COLLAPSED` | Collapsible panel | Non-negative integers |
| `EMBEDDED_PLAIN` | Inline without frame | Non-negative integers |


**Examples**

```bash
# Change a timing interval while preserving other service metadata
zeysdk update services/daily-backup.ixml --interval 720

# Change a service type; entity hooks require an entity
zeysdk update services/contact-hook.ixml \
  --type AFTER_MODIFICATION --entity contacts

# Change a weblet to an integrated type with icon metadata
zeysdk update weblets/customer-view.ixml \
  --type INTEGRATED --view customers.index \
  --color 4287F5 --svgpath 'M10 10h100v100h-100z'

# Update resource metadata
zeysdk update resources/company-logo.png \
  --name "Company Brand Logo" --public 1

# Clear optional values explicitly
zeysdk update services/remote-call.ixml --url '' --accesskey ''
zeysdk update weblets/customer-view.ixml --langaliases ''
```


**Error Handling**

| Error | Description | Resolution | Example |
|-------|-------------|------------|---------|
| Invalid asset class | Unsupported type | Use correct class | `services/backup.ixml` not `service/backup.ixml` |
| Missing app.json | Config file not found | Initialize app | Run `zeysdk create` first |
| Asset not found | Asset doesn't exist | Create asset | Run `zeysdk add` first |
| Invalid filename | Bad characters or unsafe path | Keep the path inside its asset directory | Use `services/backup-daily.ixml` |
| Extension mismatch | Path extension differs from the registered MIME type | Use the registered filename or `rename` | Do not update a `.png` resource as `.jpg` |
| Invalid type | Wrong service/weblet type | Use valid type | Use `TIMING` not `SCHEDULED` |
| Invalid schedule | Not an integer from `0` to `1439` | Use minutes from midnight | Use `1410` for 23:30 |
| Invalid interval | Not an integer of at least `1` | Use a positive minute count | Use `1440` for daily |
| Invalid dimensions | Not a non-negative integer | Use pixels without units | Use `800` not `800px` |
| Invalid color | Includes `#`, lowercase, or wrong length | Use 3 or 6 uppercase hex digits | Use `4287F5` |

**Notes**

- `update` changes metadata, not the asset file contents
- Service and weblet prompts depend on the selected type.
- Resource visibility is controlled by the `public` metadata field
- Use `zeysdk compare` after updates to review pending metadata changes
- Empty strings can clear optional URL, access-key, view, icon, and language-alias values where applicable.

---

Canonical: https://www.zeyos.com/sdk/authentication

# Authentication

The SDK accepts credentials from environment variables or from its saved host store. A local app link records the active instance in `.zeysdk`, while saved host credentials are global to the user account.

## 1. Environment Variables (Recommended for CI/CD)

Environment variables are the recommended method for CI/CD environments and automated workflows.

```bash
# Set environment variables
ZEYSDK_TOKEN=your_token
ZEYSDK_SERVER=https://cloud.zeyos.com/your_instance

# Run commands
zeysdk [command]
```

`ZEYSDK_TOKEN` overrides token lookup. `ZEYSDK_SERVER` overrides the active sync URL for commands that load sync data, including `run` and `compile`, and is also consumed by `zeysdk link`. Set both for portable non-interactive execution; a server-only override still needs a matching saved token for the active instance and URL.

### Platform-Specific Commands

**Windows CMD:**
```cmd
set ZEYSDK_TOKEN=your_token && set ZEYSDK_SERVER=https://cloud.zeyos.com/your_instance && zeysdk [command]
```

**Windows PowerShell:**
```powershell
$env:ZEYSDK_TOKEN="your_token"; $env:ZEYSDK_SERVER="https://cloud.zeyos.com/your_instance"; zeysdk [command]
```

**Linux/macOS:**
```bash
export ZEYSDK_TOKEN=your_token && export ZEYSDK_SERVER=https://cloud.zeyos.com/your_instance && zeysdk [command]
```

## 2. Stored Credentials (Default for local development)

For local development, link an application once and let the SDK reuse the saved instance-and-URL credential.

```bash
# Link your instance first
zeysdk link

# Then run commands
zeysdk [command]
```

## Authentication and URL Priority

For `zeysdk link`, explicit `--token` and `--url` flags take precedence over `ZEYSDK_TOKEN` and `ZEYSDK_SERVER`. The resulting values then take precedence over a saved host or an interactive prompt.

For commands operating on an existing link:

1. A non-empty `ZEYSDK_SERVER` replaces the URL from `.zeysdk/sync.json`.
2. A non-empty `ZEYSDK_TOKEN` replaces the saved token.
3. Without `ZEYSDK_TOKEN`, the SDK looks up a saved credential for the active instance and effective URL. Legacy instance-only entries are used only when their stored URL matches the effective URL; otherwise relink the instance or provide explicit environment credentials.

`zeysdk bundle install` deliberately requires `--token` and `--url` together when either flag is used.

## Credential Storage

Choose the backend interactively with `zeysdk configure` → `Credential Storage`:

| Mode | Behavior |
|------|----------|
| `Auto` | Uses the OS keyring on supported desktop systems. WSL and headless Linux use the file store; an unavailable desktop keyring may also fall back to it. |
| `OS Keyring` | Requires the platform keyring and reports an error when it cannot be used. |
| `File Store` | Stores credentials unencrypted in `~/.config/zeysdk/store.json` with owner-only file permissions. |

The storage preference is written to `~/.config/zeysdk/settings.json`. When the keyring is used, `~/.config/zeysdk/account-index.json` tracks account names so the CLI can enumerate configured hosts; tokens remain in the keyring.

`ZEYSDK_USE_KEYRING` overrides the saved mode. Values `1`, `true`, or `yes` select the keyring; other values select the file store. `ZEYSDK_USE_KEYTAR` is accepted as a legacy alias when the newer variable is unset.

## Expired Tokens

When most wrapped commands receive a `401`, the CLI can prompt once for a replacement token, validate the relink, update the saved credential, and retry the original command once. `link` reports its own authentication failure instead of recursively relinking, and `migrate` does not perform the automatic retry.

## Security Notes

- Stored credentials can use the OS keyring or the unencrypted file store
- Environment-variable lifetime depends on how the shell or CI system defines them
- Stored credentials persist between sessions
- Use environment variables for CI/CD to avoid storing credentials
- Use stored credentials for local development convenience
- Avoid putting tokens directly into shared scripts, command history, logs, or bundle files

## Error Handling

| Error | Description | Resolution |
|-------|-------------|------------|
| Missing token | No authentication provided for the active instance and effective URL | Set both environment variables, add a matching host, or run `zeysdk link` |
| Invalid token | Bad credentials | Check token validity |
| Server error | Invalid server URL | Verify server URL format |
| Keyring unavailable | Explicit keyring backend cannot be opened | Repair keyring access or select `Auto`/`File Store` |
| Network error | Connection failed | Check connectivity |

---

Canonical: https://www.zeyos.com/sdk/bundle-management

# Bundle Management

The Bundle Management commands help you create and install bundles of multiple ZeyOS applications.

Available Commands
------------------

| Command | Description | Usage |
|---------|-------------|-------|
| [`create`](https://www.zeyos.com/sdk/bundle-management/zeysdk-bundle-create) | Create new bundle | `zeysdk bundle create [OUTPUT_FILE]` |
| [`install`](https://www.zeyos.com/sdk/bundle-management/zeysdk-bundle-install) | Install bundle to instance | `zeysdk bundle install <BUNDLE_FILE> <INSTANCE_ID>` |

**Process**

1. **Bundle Creation**
   - Scans a parent directory for ZeyOS applications
   - Lets you choose which applications belong in the bundle
   - Writes a bundle file containing the selected app directories

2. **Bundle Installation**
   - Validates unique, contained application directory names from the bundle file
   - Links each app to the target instance
   - Force-pushes each application to the target instance sequentially
   - Reports a nonzero result if any app fails, including when `--force` allowed later apps to run

**Common Workflows**

```bash
zeysdk bundle create release-bundle --all
zeysdk bundle install release-bundle dev-instance --token "$ZEYSDK_TOKEN" --url https://cloud.zeyos.com/dev-instance
```

**Notes**

- `bundle` is a meta command; use `zeysdk bundle create` or `zeysdk bundle install`
- Bundle files store app directory names, not the full application contents
- Run bundle commands from the parent directory that contains the app folders
- Installation is not transactional across apps: an earlier successful remote push is not undone if a later app fails
- Use snapshots when you need a local backup of a single project state

---

Canonical: https://www.zeyos.com/sdk/bundle-management/zeysdk-bundle-create

# Create Bundle

The `bundle create` command creates a bundle file containing selected ZeyOS applications from the current directory.

```bash
zeysdk bundle create [OUTPUT_FILE] [options]
```

**Parameters**

| Parameter | Description | Required | Default | Example |
|-----------|-------------|----------|---------|---------|
| `OUTPUT_FILE` | Bundle filename | No | `bundle` | `my-bundle` |

**Options**

| Option | Description |
|--------|-------------|
| `-v, --verbose` | Show discovered, skipped, and selected directories |
| `--apps <list>` | Comma-separated list of app directories to include |
| `--all` | Include all ZeyOS app directories |

**Process**

1. **Directory Scanning**
   - Scans the current directory for application folders
   - Skips hidden directories
   - Ignores `node_modules` and `vendor`
   - Validates each app by checking `zeyos.app.json`

2. **App Selection**
   - Prompts for apps when no selection option is provided
   - Includes all valid apps with `--all`
   - Includes listed apps with `--apps`
   - Sorts selected apps alphabetically in the bundle file

3. **Bundle Output**
   - Writes one selected app directory per line
   - Uses `bundle` as the default filename in non-interactive mode
   - Prompts for an output filename, defaulting to `bundle`, during a fully interactive run
   - Stores relative directory names from the current folder

**Examples**

```bash
zeysdk bundle create my-bundle
zeysdk bundle create --all
zeysdk bundle create my-bundle --apps app-one,app-two
```

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| No directories | Current folder has no directories | Navigate to a folder with apps |
| No valid apps | No ZeyOS app directories found | Verify app directory structure |
| Invalid app | Requested app is missing or invalid | Check the `--apps` list |
| Invalid selection | No apps selected | Select at least one app |
| Write error | Bundle file could not be created | Check write permissions |

**Notes**

- A valid app directory must contain `zeyos.app.json`
- The app manifest must include `identifier` and `name`
- Hidden directories are skipped during discovery
- `node_modules` and `vendor` are ignored during discovery
- `--apps` expects directory names relative to the current folder
- `--all` takes precedence when used together with `--apps`
- An existing output file is replaced; choose the path carefully

**Best Practices**

- Create bundles from a stable parent folder that contains all target apps
- Use descriptive bundle filenames for release or environment targets
- Use `--all` for repeatable automation when every app in the folder should be included
- Use `--apps` when a deployment bundle should contain only selected applications

---

Canonical: https://www.zeyos.com/sdk/bundle-management/zeysdk-bundle-install

# Install Bundle

The `bundle install` command installs multiple applications from a bundle file to a ZeyOS instance.

```bash
zeysdk bundle install <BUNDLE_FILE> <INSTANCE_ID> [options]
```

**Parameters**

| Parameter | Description | Required | Example |
|-----------|-------------|----------|---------|
| `BUNDLE_FILE` | Path to bundle file | Yes | `my-bundle` |
| `INSTANCE_ID` | Target instance ID | Yes | `dev-instance` |

**Options**

| Option | Description |
|--------|-------------|
| `--token <token>` | Token for the target instance |
| `--url <url>` | URL for the target instance |
| `--yes` | Disable the missing-host configuration prompt; fail if a missing host has no token/URL pair |
| `-v, --verbose` | Show detailed validation and installation output |
| `-f, --force` | Continue to later apps after non-authentication failures |

**Process**

1. **Validation**
   - Verifies the bundle file exists
   - Validates the target instance ID
   - Requires `--token` and `--url` together when either is supplied
   - Ignores blank lines but rejects duplicate app entries
   - Accepts only safe single-directory names and verifies both their lexical paths and resolved real paths remain below the current directory
   - Verifies every listed path is a directory before changing remote state

2. **Instance Configuration**
   - Prompts to configure missing instances
   - Uses `--token` and `--url` for non-interactive setup
   - With `--yes`, fails instead of prompting when neither a matching saved host nor a token/URL pair is available
   - Validates and stores newly supplied credentials through the first successful app link rather than saving them before authentication

3. **Installation**
   - Opens each application directory
   - Links the app to the target instance
   - Switches to the target instance
   - Pushes application content
   - Restores the original working directory before processing the next app

4. **Results**
   - Reports each successful installation
   - Reports failures with the application name
   - Stops immediately on authentication failures, even with `--force`
   - With `--force`, continues after other per-app failures where possible
   - Fails the overall command if any installation failed, including partial-success runs

**Examples**

```bash
zeysdk bundle install my-bundle dev-instance
zeysdk bundle install my-bundle prod-instance --token abc123 --url https://cloud.zeyos.com/prod-instance
zeysdk bundle install my-bundle prod-instance --yes --token abc123 --url https://cloud.zeyos.com/prod-instance
zeysdk bundle install my-bundle staging --force --verbose
```

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Bundle not found | Bundle file is missing | Check file path |
| Invalid bundle | Bundle path is not a file | Use a valid bundle file |
| Empty bundle | Bundle contains no entries | Create a new bundle |
| Duplicate entry | The same app directory appears more than once | Remove duplicate lines |
| Unsafe app path | Entry is invalid, escapes the parent, or resolves outside it | Use a direct child directory name |
| Invalid instance | Instance is not configured | Configure the instance or pass `--token` and `--url` |
| Invalid token | Authentication failed | Check instance credentials |
| Directory error | App directory is missing | Verify bundle contents |
| Push failed | App installation failed | Check app configuration |

**Notes**

- Bundle installation expects each listed app directory to exist below the current directory
- `--token` and `--url` must be provided together for non-interactive instance setup
- `--yes` fails fast when the instance is missing and credentials are not provided
- Existing credentials are reused when the target instance is already configured
- Each app is pushed with force enabled during installation
- `--force` controls whether bundle processing continues; it does not change the internal force-push behavior
- Bundle installation is not an all-or-nothing transaction. Successfully pushed apps remain changed if a later app fails
- A partial-success installation returns a nonzero exit so automation cannot mistake it for a complete deployment

**Best Practices**

- Run `zeysdk bundle install` from the same parent directory used to create the bundle
- Verify the target instance credentials before installing production bundles
- Inspect bundle files as untrusted input before installation; path traversal, duplicate entries, and out-of-tree symlinks are rejected
- Use `zeysdk bundle create --apps` to keep installation scope explicit
- Use a staging instance before installing the same bundle to production

---

Canonical: https://www.zeyos.com/sdk/configuration-files

# Configuration Files

When using the SDK, you will notice that a couple of configuration files are being created:

### 1. Application Manifest (zeyos.app.json)
Describes the application's properties and represents the properties of all included assets

```json
{
  "identifier": "myapp",
  "name": "My Application",
  "version": 10000,
  "vendor": "Company Name",
  "services": {
    "reminder": {
      "type": "timing",
      "schedule": 600,
      "interval": 60
    }
  },
  "resources": {
    "logo": {
      "mimetype": "image/png",
      "public": 1
    }
  },
  "weblets": {
    "dashboard": {
      "type": "popup_framed",
      "width": 800,
      "height": 600
    }
  }
}
```

### 2. The ZeyOS Sync File (sync.json)

The `sync.json` file is created whenever you are syncing an application with a ZeyOS instance.

The instance configuration stores the authentication information when executing `zeysdk link [INSTANCE_ID]`. By using `zeysdk use [INSTANCE_ID]` a user can switch between different instances.


```json
Example:

{
  "id": "instance-id",
  "url": "platform-url",
  "syncstates": {
    "settings": {
      "hash": "file-hash",
      "lastmodified": "timestamp"
    },
    "defaultsettings": {
      "hash": "file-hash",
      "lastmodified": "timestamp"
    },
    "services": {},
    "resources": {},
    "weblets": {}
  }
}

```
The sync configuration consists of two core sections:

<details>
<summary><strong>1. Connection Settings</strong></summary>

```json
{
  "id": "instance-id",
  "url": "https://platform-url"
}
```
- Updated automatically when running `zeysdk link`
- Contains platform URL
- Manages instance connection details
</details>

<details>
<summary><strong>2. Sync States</strong></summary>

```json
{
  "syncstates": {
    "settings": {
      "hash": "file-hash",
      "lastmodified": 1234567890
    },
    "services": {},
    "resources": {},
    "weblets": {}
  }
}
```
- Tracks file states for settings, resources, services, weblets, and assets
- Uses hashes for change detection
- Stores the server-returned `lastmodified` timestamp for synced items
- Manages synchronization between local and server files
</details>

### Sync Scenarios

<details>
<summary><strong>New Files (Empty States)</strong></summary>

When no sync hash exists:

| Scenario | Action | Resolution Options |
|----------|--------|-------------------|
| File doesn't exist on server | Auto-create | N/A |
| File exists on server | Conflict | • `theirs`: Use server version• `mine`: Use local version |
</details>

<details>
<summary><strong>Existing Files (With States)</strong></summary>

When a sync hash exists:

| Scenario | Action | Resolution Options |
|----------|--------|-------------------|
| File missing on server | Conflict | • `theirs`: Remove locally• `mine`: Create on server |
| States don't match | Conflict | • `theirs`: Use server version• `mine`: Use local version |
| States match | Auto-sync | Copy to server |
</details>

**We highly recommend using the SDK's interface to edit those file and restrain from
editing them directly.**

However, in some cases you might want to automate certain tasks through additional
scripts and task runners - in such cases it is certainly useful to have a better
understanding about the underlying file structure and properties.

**BE CAREFUL WHEN MANIPULATING THE SYNC STATES, YOU ARE PUTTING YOUR SYSTEM INTEGRITY AT RISK!**

---

Canonical: https://www.zeyos.com/sdk/debugging

# Debugging and Compilation

Execute a local iXML or Zymba script on the active linked ZeyOS instance. Run this command from an app containing `zeyos.app.json` and `.zeysdk/sync.json`.

**Execution Context**

`run` is console execution with application globals. It sends the local source to the instance and executes it there.

- Effects are real. Database writes, mail, and external calls happen, and there is no dry-run mode.
- Your local file runs even if it was never pushed, but anything it includes resolves to assets deployed on the instance.
- The app's namespace and settings are available. There is no user session, no service `AUTH` context, and no inbound HTTP request, so `REQUEST`, `SELFURL`, and `VIEW` are absent.
- It does not simulate a weblet or a service route. Console requests use PUT, so `GET` and `POST` routes in a `rest:server` block do not match.

Test weblets, services, schedules, and hooks through their real entry points. See [Execution Contexts](https://www.zeyos.com/ixml/ixml-guide/development-environment/execution-contexts).

**Timeouts**

Requests time out after 30 seconds by default; set `ZEYSDK_REQUEST_TIMEOUT_MS` to a positive integer to change this. The timeout ends the client's wait. It does not show that the server stopped, so the script may still be running or may have finished. `run` is not resent automatically. Check the script's effects before running it again.

```bash
# Run a script asset
zeysdk run [ASSET_PATH] [options]

# Run inline iXML (auto-wrapped in default iXML document)
zeysdk run --ixml '<output>hello</output>' [options]

# Run inline Zymba
zeysdk run --zymba '/* your code */' [options]

# Equivalent short alias
zeysdk run --zy '/* your code */' [options]
```

**Parameters**

| Parameter | Description | Required | Example |
|-----------|-------------|----------|---------|
| `ASSET_PATH` | Script path including class directory and filename | Yes, unless one inline option is used | `services/daily-backup.ixml` |

**Options**

| Option | Description | Default |
|--------|-------------|---------|
| `--verbose` | Show detailed execution progress and stack traces | `false` |
| `--clean` | Print only console output (no metrics) | `false` |
| `--unpretty` | Use simple metadata formatting for this run | `false` |
| `--ixml <code>` | Run inline iXML (wrapped in default iXML document) | - |
| `--zymba <code>` | Run inline Zymba code | - |
| `--zy <code>` | Alias for `--zymba` | - |

**Input Rules**

- File paths must begin with `services`, `resources`, `weblets`, or `assets` and use `.ixml`, `.zy`, or `.zymba`.
- Paths must remain inside their asset-class directory. Absolute paths, traversal, invalid characters, and symbolic-link components are rejected.
- Supply either `ASSET_PATH` or exactly one of `--ixml`, `--zymba`, and `--zy`.
- Inline code cannot be empty. iXML fragments are inserted into the default iXML wrapper; a complete iXML document is used unchanged.
- A file does not need a manifest asset entry, but it must exist locally and be readable.

**Execution Results**

The command outputs detailed execution metrics:

```bash
=== Run Results ===
Context
  Platform | sdk-alpha (https://cloud.zeyos.com/sdk-alpha/)
  Engine   | iXML
Metrics
  Load Time      | 1.2 ms
  Execution Time | 45.3 ms
  Memory Usage   | 2.5 MB
  Output Size    | 128 KB
Response
  HTTP Status | 200
Output
  Operation completed successfully
```

| Metric | Description | Format |
|--------|-------------|---------|
| `Platform` | Active linked platform | String |
| `Load Time` | Script loading duration | milliseconds/seconds |
| `Execution Time` | Processing duration | milliseconds/seconds |
| `Memory Usage` | Peak memory consumption | Bytes/KB/MB/GB |
| `Output Size` | Result data size | Bytes/KB/MB/GB |
| `Engine` | Execution engine version | String |
| `Output` | Script execution result | String |
| `Debug Output` | Debug information | String |
Server-reported execution errors are written to standard error and return a nonzero exit status instead of rendering a results block.

**Examples**

```bash
# Run a service script
zeysdk run services/backup.ixml --verbose
# Output:
# Input validation completed.
# Loading sync info and package info.
# Making REST call to run file on the server.
# Run completed successfully.
# === Run Results ===
# Context
#   Platform | sdk-alpha (https://cloud.zeyos.com/sdk-alpha/)
#   Engine   | iXML
# Metrics
#   Load Time      | 0.8 ms
#   Execution Time | 123.4 ms
#   Memory Usage   | 1.2 MB
# Output
#   Backup completed successfully

# Run a Zymba weblet script (`.zymba` is also accepted)
zeysdk run weblets/dashboard.zy
# Output:
# === Run Results ===
# Context
#   Platform | sdk-alpha (https://cloud.zeyos.com/sdk-alpha/)
# Metrics
#   Load Time      | 1.1 ms
#   Execution Time | 67.2 ms
#   Memory Usage   | 3.4 MB
# Output
#   Dashboard rendered successfully

# Run inline iXML
zeysdk run --ixml '<output>Hello</output>'

# Run inline Zymba with the short alias
zeysdk run --zy 'return "Hello";'

# Print only script output
zeysdk run services/backup.ixml --clean

# Force simple metadata output
zeysdk run services/backup.ixml --unpretty
```


**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Invalid asset class | Wrong type specified | Use correct asset class |
| Missing sync.json | App is not linked | Run `zeysdk link` first |
| Invalid sync.json | Linked instance lacks `id` or `url` | Relink the instance |
| Missing script | Local file is missing or unreadable | Check the path and permissions |
| Unsafe path | Path escapes its asset directory or uses a symbolic link | Use an in-app path |
| Invalid script type | Wrong extension | Use `.ixml`, `.zy`, or `.zymba` |
| Invalid usage | Missing input, multiple inline flags, or mixed path and inline input | Supply exactly one input source |
| Authentication error | Token expired | Auto-relinks |
| Execution error | Server returned a runtime error | Check standard error; add `--verbose` for a stack trace |

By default, stack traces from execution errors are hidden. Use `--verbose` to include them. `--clean` writes only script console output to standard output; errors still go to standard error and exit nonzero. When `--clean` is present, progress messages are suppressed.

Default output style is pretty. Set a global default under `zeysdk configure` -> `Run Output Style`, or override per command with `--unpretty`.

Compile Scripts (zeysdk compile)
================================

Compile a local or inline iXML/Zymba script on the linked instance without executing it.

Compilation is not offline: the command transmits the source to the instance's authenticated compiler, so it needs a linked app, a valid token, and network access. A successful compile does not show that database identifiers exist, that `include` targets are deployed, or that the code behaves correctly at runtime. See [Verification Checkpoints](https://www.zeyos.com/ixml/ixml-guide/development-environment/verification-checkpoints).

```bash
zeysdk compile [ASSET_PATH] [options]
zeysdk compile --ixml '<output>Hello</output>'
zeysdk compile --zy 'return "Hello";'
```

`ASSET_PATH` and inline input follow the same extension, path-safety, and mutual-exclusion rules as `run`. Compile also requires both the app manifest and a valid linked sync file.

| Option | Description |
|--------|-------------|
| `--verbose` | Show validation, API, and completion progress |
| `--ixml <code>` | Compile inline iXML, wrapping a fragment in the default document |
| `--zymba <code>` | Compile inline Zymba |
| `--zy <code>` | Alias for `--zymba` |
| `--phpout [OUTPUT_FILE]` | Print returned PHP to standard output, or write it to `OUTPUT_FILE` |
| `--intermediate` | Request and print returned intermediate Z-code |

```bash
# Show compilation summary
zeysdk compile services/backup.ixml

# Print only the returned PHP when available
zeysdk compile services/backup.ixml --phpout

# Write returned PHP to a file
zeysdk compile services/backup.ixml --phpout compiled.php

# Request and display intermediate Z-code
zeysdk compile services/backup.ixml --intermediate
```

A server-reported compilation error is written to standard error and exits nonzero. It is checked before PHP output, so `--phpout OUTPUT_FILE` does not write code from an error response. A local output-file error also exits nonzero.

Lint Project Scripts (zeysdk lint)
==================================

Compile eligible project `.ixml`, `.zy`, and `.zymba` files and summarize each result. The file set comes from the app manifest plus standard local settings files.

```bash
zeysdk lint [--all] [--verbose]
```

By default, `lint` fetches server state and checks locally present scripts that differ from the last synchronized state. `--all` checks every eligible locally present script without first fetching server state. The command requires a linked app and rejects unreadable or unsafe manifest paths rather than silently skipping them.

Like `compile`, `lint` transmits source to the remote compiler and does not execute it. `--all` skips the server-state comparison, not the compile requests.

Each script is reported as `OK` or `ERROR`. Compilation and network failures are reported per file; the command exits with status `1` if any file fails. Finding no eligible scripts is informational and exits successfully, but it is not a pass: nothing was checked. Scripts that are not registered in the manifest are not selected, so compile those by path.

---

Canonical: https://www.zeyos.com/sdk/entities

# ZeyOS Entities

- `accounts`
- `actionsteps`
- `addresses`
- `applications`
- `appointments`
- `associations`
- `campaigns`
- `categories`
- `comments`
- `components`
- `contacts`
- `contacts2contacts`
- `contracts`
- `couponcodes`
- `coupons`
- `davservers`
- `dunning`
- `dunning2transactions`
- `events`
- `feedservers`
- `fields`
- `files`
- `follows`
- `groups`
- `groups2users`
- `invitations`
- `items`
- `likes`
- `links`
- `mailinglists`
- `mailservers`
- `messagereads`
- `messages`
- `notes`
- `numcounters`
- `objects`
- `opportunities`
- `participants`
- `payments`
- `permissions`
- `pricelists`
- `pricelists2accounts`
- `prices`
- `projects`
- `records`
- `resources`
- `services`
- `stocktransactions`
- `storages`
- `suppliers`
- `tasks`
- `tickets`
- `transactions`
- `users`
- `weblets`

---

Canonical: https://www.zeyos.com/sdk/platform-management

# Platform Management

The Platform Management commands help you connect to and manage ZeyOS instances.

Available Commands
------------------

| Command | Description | Usage |
|---------|-------------|-------|
| [`link`](https://www.zeyos.com/sdk/platform-management/zeysdk-link) | Connect to instance | `zeysdk link [INSTANCE_ID]` |
| [`unlink`](https://www.zeyos.com/sdk/platform-management/zeysdk-unlink) | Remove instance connection | `zeysdk unlink [INSTANCE_ID]` |
| [`use`](https://www.zeyos.com/sdk/platform-management/zeysdk-use) | Switch active instance | `zeysdk use [INSTANCE_ID]` |
| [`whoami`](https://www.zeyos.com/sdk/platform-management/zeysdk-whoami) | Show current instance | `zeysdk whoami` |

Links are local to the current application. Saved host credentials are global and are managed separately with `zeysdk host`.

---

Canonical: https://www.zeyos.com/sdk/platform-management/zeysdk-link

# Link Instance

The `link` command connects your local application to a ZeyOS instance and sets up synchronization.

```bash
zeysdk link [INSTANCE_ID] [options]
```

**Parameters**

| Parameter | Description | Required | Example |
|-----------|-------------|----------|---------|
| `INSTANCE_ID` | ZeyOS instance identifier | No | `myinstance` |

**Options**

| Option | Description |
|--------|-------------|
| `--token <token>` | SDK authentication token |
| `--url <url>` | Instance URL |
| `--break-lock` | Replace the current sync lock after you verify its operation is no longer running |
| `--verbose` | Show detailed output |

**Process**

1. **Connection Setup**
   - Requires the current directory to contain `zeyos.app.json` with `identifier` and `name`
   - Resolves the instance ID, URL, and token from arguments, environment variables, saved hosts, or prompts
   - Defaults the URL to `https://cloud.zeyos.com/<INSTANCE_ID>`
   - Can infer a missing instance ID from the server URL

2. **Authentication**
   - Validates the SDK token
   - Fetches the remote application, or creates it when it does not yet exist
   - Saves credentials only after the link operation succeeds
   - During an expired-token relink, validates the new token before replacing the saved credential; an obsolete URL-keyed entry is removed afterward

3. **Sync Setup**
   - Creates or updates `.zeysdk/sync.json`
   - Keeps the active link in `.zeysdk/sync.json` and other linked instances in `.zeysdk/sync.<INSTANCE_ID>.json`
   - Creates `settings/settings.<INSTANCE_ID>.json` when absent
   - Makes the newly linked instance active
   - Restores those local files if link setup fails
   - Holds the shared operation lock through remote validation, local state changes, and credential persistence
   - Makes the instance available for push, pull, sync, and activity commands

**Examples**

```bash
zeysdk link myinstance
zeysdk link myinstance --url https://custom.domain/app
zeysdk link myinstance --token "$ZEYSDK_TOKEN" --url https://cloud.zeyos.com/myinstance --verbose

# The same values can be provided by the environment
ZEYSDK_TOKEN=abc123 ZEYSDK_SERVER=https://cloud.zeyos.com/myinstance zeysdk link myinstance
```

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Missing token | No SDK token provided | Enter a token when prompted or use `--token` |
| Invalid instance | Instance ID contains unsupported characters | Use letters, numbers, dots, underscores, and hyphens, beginning and ending with a letter or number |
| Invalid URL | Instance URL cannot be reached | Check `--url` or instance ID |
| Auth failed | Invalid token | Generate a new SDK token |
| Save failed | Sync configuration could not be written | Check local permissions |

**Notes**

- `INSTANCE_ID` can be provided as an argument or resolved interactively
- `--url` overrides the default cloud URL for the instance
- Command-line flags take precedence over `ZEYSDK_TOKEN` and `ZEYSDK_SERVER`
- Pass both token and URL for predictable non-interactive setup unless a matching saved host supplies the missing value
- Unlike `zeysdk clone`, a normal link creates the remote application if it is missing
- Linking does not automatically push local files
- If the remote app is created and a later local write fails, local files are rolled back but the remote creation is not automatically undone

**Best Practices**

- Link development and production instances with clear IDs
- Use `zeysdk whoami` after linking to verify the active instance
- Avoid sharing SDK tokens in shell history when possible
- Run `zeysdk compare` before the first push to an existing remote app

---

Canonical: https://www.zeyos.com/sdk/platform-management/zeysdk-unlink

# Unlink Instance

The `unlink` command removes a local connection to a ZeyOS instance.

```bash
zeysdk unlink [INSTANCE_ID] [options]
```

**Parameters**

| Parameter | Description | Required | Example |
|-----------|-------------|----------|---------|
| `INSTANCE_ID` | Instance to unlink | No | `myinstance` |


**Options**

| Option | Description |
|--------|-------------|
| `--break-lock` | Replace the current sync lock after you verify its operation is no longer running |
| `--verbose` | Show detailed output |

**Process**

1. **Validation**
   - Loads linked instances
   - Uses the active instance automatically when `INSTANCE_ID` is omitted
   - Verifies that the selected instance exists

2. **Unlinking**
   - Removes the explicit non-active instance file, or removes `.zeysdk/sync.json` when unlinking the active instance
   - Prompts for a replacement before unlinking the active instance when multiple other links exist
   - Leaves no active instance when no linked instances remain
   - Restores affected sync files if the local unlink operation fails

**Examples**

```bash
zeysdk unlink myinstance
zeysdk unlink
```

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Missing sync.json | No active instance configuration | Run `zeysdk link` |
| Invalid instance | Instance is not linked | Choose a linked instance |
| Delete failed | Sync files could not be removed | Check local permissions |

**Notes**

- `unlink` removes local connection state for the selected instance
- Remote applications and files are not deleted
- Globally saved host credentials are not deleted; use `zeysdk host remove` for that
- If the active instance is unlinked, another linked instance can become active
- When no linked instances remain, no active instance is set

**Best Practices**

- Use `zeysdk whoami` before unlinking the active instance
- Unlink old development instances that are no longer used
- Keep production instance names easy to recognize
- `unlink` refuses to mutate instance state while another synchronization or instance-state operation owns the project lock

---

Canonical: https://www.zeyos.com/sdk/platform-management/zeysdk-use

# Use Instance

The `use` command switches the active ZeyOS instance for the current application.

```bash
zeysdk use [INSTANCE_ID] [options]
```

**Parameters**

| Parameter | Description | Required | Example |
|-----------|-------------|----------|---------|
| `INSTANCE_ID` | Instance to switch to | No | `myinstance` |

**Options**

| Option | Description |
|--------|-------------|
| `--break-lock` | Replace the current sync lock after you verify its operation is no longer running |
| `--verbose` | Show detailed output |

**Process**

1. **Validation**
   - Loads linked instances
   - Prompts for an instance when none is provided
   - Verifies that the selected instance exists

2. **Activation**
   - Renames the current active sync file to its instance-specific filename
   - Makes the selected instance-specific sync file active as `.zeysdk/sync.json`
   - Restores the affected sync files if switching fails
   - Clears cached views for the new instance

**Examples**

```bash
zeysdk use myinstance
zeysdk use
```

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Missing sync.json | No active instance configuration | Run `zeysdk link` |
| Invalid instance | Instance is not linked | Choose a linked instance |
| Switch failed | Sync files could not be updated | Check local permissions |

**Notes**

- `use` changes the active local instance for the current application
- `use` refuses to switch while another synchronization or instance-state operation owns the project lock
- The command does not modify remote application data
- Cached views are cleared after switching instances
- Commands such as `push`, `pull`, `sync`, and activity changes use the active instance

**Best Practices**

- Run `zeysdk whoami` after switching to confirm the target instance
- Keep instance IDs descriptive enough to avoid confusing environments
- Compare before pushing after switching to a different instance
- Avoid switching instances while another CLI operation is running

---

Canonical: https://www.zeyos.com/sdk/platform-management/zeysdk-whoami

# Show Current Instance

The `whoami` command displays the active ZeyOS instance for the current application.

```bash
zeysdk whoami [options]
```

**Options**

| Option | Description |
|--------|-------------|
| `--verbose` | Show detailed output |

**Process**

1. **Validation**
   - Loads `.zeysdk/sync.json`
   - Verifies that an active instance is configured

2. **Output**
   - Displays the active instance as `URL: ...`
   - Displays the active instance as `ID: ...`


**Examples**

```bash
zeysdk whoami
zeysdk whoami --verbose
```

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Missing sync.json | No active instance configuration | Run `zeysdk link` |
| Invalid sync.json | Active instance configuration is invalid | Re-link the instance |

**Notes**

- `whoami` reads the active instance from `.zeysdk/sync.json`
- It does not validate credentials or contact the remote instance
- The command is useful before operations that modify remote state
- Use verbose output when troubleshooting local sync configuration

**Best Practices**

- Run `zeysdk whoami` before destructive or production-facing commands
- Confirm the instance before `push`, `sync`, `delete`, `deactivate`, or `predelete`
- Re-link the instance if the displayed URL or ID is incorrect

---

Canonical: https://www.zeyos.com/sdk/snapshot-management

# Snapshot Management

The Snapshot Management commands help you create, manage, and restore backups of your ZeyOS applications.

Available Commands
------------------

| Command | Description | Usage |
|---------|-------------|-------|
| [`list`](https://www.zeyos.com/sdk/snapshot-management/zeysdk-snapshot-list) | List available snapshots | `zeysdk snapshot list` |
| [`create`](https://www.zeyos.com/sdk/snapshot-management/zeysdk-snapshot-create) | Create new snapshot | `zeysdk snapshot create [COMMENT]` |
| [`restore`](https://www.zeyos.com/sdk/snapshot-management/zeysdk-snapshot-restore) | Restore from snapshot | `zeysdk snapshot restore [SNAPSHOT_ID]` |
| [`delete`](https://www.zeyos.com/sdk/snapshot-management/zeysdk-snapshot-delete) | Delete snapshot | `zeysdk snapshot delete <SNAPSHOT_ID>` |

**Process**

1. **Snapshot Creation**
   - Archives non-hidden project entries plus the supported `.zeysdk` manifest files
   - Stores snapshot metadata with comment and timestamp
   - Saves the completed ZIP and metadata in `.zeysdk/snapshots`

2. **Snapshot Management**
   - Lists available snapshots with ID, comment, and date
   - Supports restore and delete actions from the interactive list
   - Lets you restore a selected snapshot by ID

3. **Snapshot Restore**
   - Confirms before replacing local files unless forced
   - Validates and extracts the archive into a sibling staging directory before touching the target
   - Preserves the target's root `.zeysdk` contents, then overlays `.zeysdk/assets.json` and `.zeysdk/package.json` when the snapshot contains them
   - Swaps the staged tree into place and rolls the original directory back if the commit step fails

**Common Workflows**

```bash
zeysdk snapshot create "Before payment changes"
zeysdk snapshot list
zeysdk snapshot restore 20231101-123456
zeysdk snapshot delete 20231101-123456
```

**Notes**

- `snapshot` is a meta command; use `zeysdk snapshot list`, `create`, `restore`, or `delete`
- Snapshot IDs use timestamp-style names such as `20231101-123456`; same-second collisions add `-1`, `-2`, and so on
- Snapshot IDs accepted by restore/delete are constrained to safe filenames and cannot contain directory separators
- Create snapshots before major changes to a current-format project; separately preserve legacy hidden metadata before migration
- Use bundles when you need to install multiple applications to an instance

---

Canonical: https://www.zeyos.com/sdk/snapshot-management/zeysdk-snapshot-create

# Create Snapshot

The `snapshot create` command creates a backup of the current project state.

```bash
zeysdk snapshot create [COMMENT]
```

**Parameters**

| Parameter | Description | Required | Example |
|-----------|-------------|----------|---------|
| `COMMENT` | Description of the snapshot | No | `"Initial setup"` |

**Process**

1. **Comment Collection**
   - Uses the provided `COMMENT` argument when present
   - Prompts for a comment when no argument is provided
   - Requires a non-empty comment
   - Press Escape at the comment prompt to cancel without creating an archive or metadata

2. **Archive Creation**
   - Creates the local snapshot directory when needed
   - Generates `YYYYMMDD-HHMMSS.zip`, adding a numeric suffix if that name already exists
   - Reserves the output filename exclusively instead of overwriting an existing snapshot
   - Creates a compressed `.zip` archive and waits for the output stream to close
   - Removes the partial ZIP if archive creation fails

3. **Metadata Update**
   - Updates metadata only after the ZIP has fully closed
   - Stores the unique snapshot ID
   - Stores the comment
   - Stores the creation timestamp

**Examples**

```bash
zeysdk snapshot create "Initial setup"
zeysdk snapshot create
zeysdk snapshot create "Release v2.1.0"
zeysdk snapshot create "Pre-update: Payment API"
```

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Missing comment | No comment entered at prompt | Enter a non-empty comment |
| Snapshot write error | Archive could not be created | Check write permissions |
| Archive error | File compression failed | Check file access and disk space |
| Metadata error | Snapshot metadata could not be updated | Check snapshot storage permissions |

**Notes**

- Snapshot IDs are generated from local date and time; collision suffixes preserve both same-second snapshots
- The normal project glob includes dotfiles and excludes root `.git`, `node_modules`, and `.zeysdk/**`
- Both `.git` directories and worktree `.git` pointer files are excluded
- `.zeysdk/assets.json` and `.zeysdk/package.json` are added explicitly when present
- `settings/defaultsettings.json` is included once through the normal project glob
- Other hidden files and directories are included unless covered by the exclusions above
- Generated directories such as `dist` and `build` are included; root `node_modules` is excluded
- Restore preserves the destination's current root `.git` and `node_modules` by moving them into the restored tree without copying their contents. Git history, stashes, and worktree identity survive; dependencies may need updating for restored manifests
- Restore rejects archives containing reserved root `.git` or `node_modules` entries, including older archives containing a worktree pointer. These entries must not replace destination operational state
- Preservation and archive validation use existing dependencies only
- Snapshot archives are compressed as ZIP files

**Best Practices**

- Create a snapshot before large refactors to a current-format project
- Use comments that describe the reason for the snapshot
- Include release, task, or change names in comments when helpful
- Create a fresh snapshot after a stable milestone

---

Canonical: https://www.zeyos.com/sdk/snapshot-management/zeysdk-snapshot-delete

# Delete Snapshot

The `snapshot delete` command deletes a specific snapshot.

```bash
zeysdk snapshot delete <SNAPSHOT_ID>
```

**Parameters**

| Parameter | Description | Required | Example |
|-----------|-------------|----------|---------|
| `SNAPSHOT_ID` | ID of snapshot to remove | Yes | `20231101-123456` |

**Process**

1. **Snapshot Lookup**
   - Accepts IDs with or without the `.zip` suffix
   - Accepts only a safe snapshot filename and rejects directory separators or traversal
   - Verifies the resolved archive remains within `.zeysdk/snapshots`
   - Stops when the snapshot does not exist

2. **Deletion**
   - Removes the snapshot archive
   - Removes the snapshot metadata entry
   - Reports successful removal

**Examples**

```bash
zeysdk snapshot delete 20231101-123456
```

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Invalid ID | ID is empty, contains unsafe characters, or contains a path | Copy the ID from `snapshot list` |
| Not found | Snapshot archive does not exist | Check snapshot list |
| Access denied | Permission issue | Check file permissions |

**Notes**

- Delete removes the snapshot archive from local storage
- Delete also removes the matching metadata entry
- The archive is first renamed to a same-directory tombstone; if the atomic metadata update fails, the original archive name is restored
- Invalid or unreadable snapshot metadata stops deletion before the archive is moved
- Delete does not prompt for confirmation when called directly
- Snapshot deletion cannot be undone
- Use `zeysdk snapshot list` to find the exact snapshot ID

**Best Practices**

- Keep snapshots that mark stable milestones
- Remove old snapshots after they are no longer needed
- Verify a newer snapshot exists before deleting an older known-good one
- Avoid deleting snapshots during active rollback work

---

Canonical: https://www.zeyos.com/sdk/snapshot-management/zeysdk-snapshot-list

# List Snapshots

The `snapshot list` command displays existing snapshots and supports interactive snapshot management.

```bash
zeysdk snapshot list
```

**Features**

| Feature | Description |
|---------|-------------|
| List | View all snapshots with comments and dates |
| Restore | Revert to a selected snapshot |
| Delete | Remove selected snapshot |
| Details | View snapshot metadata |

**Process**

1. **Metadata Loading**
   - Reads local snapshot metadata
   - Exits when no snapshots are available
   - Formats snapshot rows for interactive display

2. **Snapshot Selection**
   - Displays snapshot ID, comment, and date
   - Uses configured list height when available
   - Prompts you to select a snapshot

3. **Action Selection**
   - Shows details for the selected snapshot
   - Offers restore or delete actions
   - Confirms deletion before removing a snapshot
   - Press Escape in a restore/delete confirmation to return to snapshot actions without making changes
   - Press Escape in snapshot actions to return to the snapshot list, or in the list to exit
   - Declining deletion returns to snapshot actions, with Delete still selected
   - Returning to either menu preserves its selected row

**Display Format**

| Column | Description |
|--------|-------------|
| ID | Snapshot identifier (`YYYYMMDD-HHMMSS`, optionally with a collision suffix) |
| Comment | User-provided description |
| Date | Creation timestamp |

**Examples**

```bash
zeysdk snapshot list
```

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| No snapshots | Snapshot metadata is empty | Create a snapshot first |
| Metadata error | Snapshot metadata cannot be read | Check snapshot metadata file |
| Restore failed | Selected restore action failed | Check snapshot archive and permissions |
| Delete failed | Selected delete action failed | Check file permissions |

**Notes**

- Snapshot IDs are displayed without the `.zip` suffix
- Missing or invalid comments and dates display as `No comment` and `No date`
- The list is driven by `.zeysdk/snapshots/snapshots.json`; an orphan ZIP without metadata is not displayed
- The interactive list supports restore and delete actions
- Delete actions ask for confirmation before removing the archive
- Restore actions use the current working directory as the restore target

**Best Practices**

- Review comments before restoring a snapshot
- Keep comments specific enough to identify the project state
- Delete old snapshots that are no longer useful
- Keep at least one known-good snapshot before risky local changes

---

Canonical: https://www.zeyos.com/sdk/snapshot-management/zeysdk-snapshot-restore

# Restore Snapshot

The `snapshot restore` command reverts a project to a previous snapshot state.

```bash
zeysdk snapshot restore [SNAPSHOT_ID] [options]
```

**Parameters**

| Parameter | Description | Required | Example |
|-----------|-------------|----------|---------|
| `SNAPSHOT_ID` | ID of snapshot to restore | No | `20231101-123456` |

**Options**

| Option | Description |
|--------|-------------|
| `--dir <directory>` | Target directory |
| `-f, --force` | Skip confirmation |

**Process**

1. **Snapshot Selection**
   - Uses `SNAPSHOT_ID` when provided
   - Shows an interactive snapshot list when no ID is provided
   - Stops when no snapshot is selected
   - Press Escape in the picker to exit
   - Accepts the ID with or without `.zip`, but rejects paths, traversal segments, and unsafe filename characters

2. **Confirmation**
   - Asks before restoring by default
   - Skips the prompt with `--force`
   - Warns that current changes will be overwritten
   - Press Escape to return to the picker with the same snapshot selected, or to exit when an ID was supplied directly

3. **Validation and Staging**
   - Opens the ZIP and rejects absolute, parent-traversal, and out-of-target entries
   - Rejects archived root `.git` and `node_modules` entries so snapshots cannot replace destination operational state
   - Extracts the complete archive to a uniquely named sibling staging directory
   - Leaves the target untouched if validation or extraction fails

4. **Root State Merge and Commit**
   - Copies the target's existing root `.zeysdk` into the staged tree
   - Overlays snapshot copies of `.zeysdk/assets.json` and `.zeysdk/package.json`
   - Does not specially preserve nested `.zeysdk` directories elsewhere in the old project
   - Renames the original target to a temporary backup and the staged tree to the target
   - Moves the target's root `.git` and `node_modules` into the staged tree during the guarded swap, without copying their contents; preserves Git directories, worktree pointer files, and dependency symlinks
   - Restores the original target and its operational state if the staged rename fails; retains and reports recovery paths if rollback itself fails

**Examples**

```bash
zeysdk snapshot restore 20231101-123456
zeysdk snapshot restore 20231101-123456 -f
zeysdk snapshot restore 20231101-123456 --dir ./test-env
zeysdk snapshot restore
```

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Not found | Snapshot ID does not exist | Check `zeysdk snapshot list` |
| No selection | No snapshot selected interactively | Select a snapshot or pass `SNAPSHOT_ID` |
| Access denied | Target files cannot be replaced | Check file permissions |
| Extract failed | Snapshot archive could not be extracted | Verify the snapshot archive |
| Unsafe archive | ZIP contains a path outside the restore root | Do not restore the archive; recreate it from a trusted project |
| Unsafe target | Target is a filesystem root, symlink, or parent of the running CLI directory | Choose a normal project directory and run from a safe location |
| Cancelled | Restore prompt was declined | Re-run with confirmation or `--force` |

**Warning**

Restore operation will:
- Overwrite existing files
- Remove additional files
- Cannot be undone

**Notes**

- `--dir` defaults to the current directory
- The snapshot archive is always read from `.zeysdk/snapshots` under the directory where the command was started, even when `--dir` points elsewhere
- Restore preserves the target's root `.git`, `node_modules`, and `.zeysdk`; archived supported manifest files take precedence over their current versions
- Git history, branches, index, and stashes remain current; snapshot restoration changes the working files, not the Git revision. Existing dependencies remain installed and may need updating if the restored dependency manifests differ
- Additional old target files, including nested `.zeysdk` directories, are removed by the successful directory swap
- Runtime extraction and commit errors are rollback-safe, but a completed restore intentionally replaces the old project tree
- If the snapshot does not exist, the command reports the missing ID and stops

**Best Practices**

- Run `zeysdk snapshot list` before restoring if you are unsure which snapshot to use
- Create a new snapshot before restoring when current work may still be needed
- Use `--dir` to inspect a restore in a separate folder
- Use `--force` only in scripted or fully verified workflows

---

Canonical: https://www.zeyos.com/sdk/synchronization

# Synchronization

The Synchronization commands help you manage file states between your local development environment and remote ZeyOS instances.

Available Commands
------------------

| Command | Description | Usage |
|---------|-------------|-------|
| [`compare`](https://www.zeyos.com/sdk/synchronization/zeysdk-compare) | Compare local and remote state | `zeysdk compare [ASSET_PATH...] [options]` |
| [`pull`](https://www.zeyos.com/sdk/synchronization/zeysdk-pull) | Get latest from instance | `zeysdk pull [ASSET_PATH...] [options]` |
| [`push`](https://www.zeyos.com/sdk/synchronization/zeysdk-push) | Upload local changes | `zeysdk push [ASSET_PATH...] [options]` |
| [`sync`](https://www.zeyos.com/sdk/synchronization/zeysdk-sync) | Synchronize local and remote state | `zeysdk sync [ASSET_PATH...] [options]` |
| [`watch`](https://www.zeyos.com/sdk/synchronization/zeysdk-watch) | Watch for changes | `zeysdk watch [options]` |
| [`rollout`](https://www.zeyos.com/sdk/application-management/zeysdk-rollout) | Push the application to linked instances | `zeysdk rollout [INSTANCE_ID...] [options]` |
| [`recover`](https://www.zeyos.com/sdk/synchronization/zeysdk-recover) | List, restore, or delete sync-state backups | `zeysdk recover [options]` |

Shared Synchronization Safety
-----------------------------

`pull`, `push`, `sync`, the instance-switching and push phase of `rollout`, and every `recover` operation use the project lock at `.zeysdk/.sync.lock`. The lock is created atomically and contains an owner token, so one operation cannot release a lock owned by another. Locks are released when an operation succeeds, fails, or is cancelled.

If the lock belongs to a running process, wait for that operation to finish. Stale locks are reclaimed automatically. `pull`, `push`, `sync`, `rollout`, `link`, `unlink`, and `use` also expose `--break-lock`, but use it only after confirming that the recorded process is no longer performing work. `link`, `unlink`, and `use` acquire the same lock so the active instance cannot change during synchronization. `compare` does not acquire the lock. `watch` acquires it for each automatic push rather than for the lifetime of the watcher.

Dry Runs
--------

`pull --dry-run`, `push --dry-run`, and `sync --dry-run` scan local state and read server state, then report planned work without applying server mutations or changing project files, the application manifest, or `sync.json`. They also skip creation of a `sync.json` backup. If `--debug-log` is supplied, diagnostic output is still written to `.zeysdk/debug.log`. A sync dry-run may remove an SDK-owned merge temporary file older than one hour from the system temporary directory.

There is no dry-run option for `watch` or `rollout`. `compare` is the read-only preview command.

Backups and Recovery
--------------------

Before a non-dry-run `pull`, `push`, or `sync`, the CLI rotates a small set of `.zeysdk/sync.json.backup*` files. On a reported failure it attempts to restore the newest one. These backups contain only `sync.json`; they do not undo local file changes, manifest changes, or server changes that happened before a later step failed. Use [`recover`](https://www.zeyos.com/sdk/synchronization/zeysdk-recover) to inspect or restore them manually.

Rollout's optional timestamped ZIP under `.zeysdk/backups/` is separate. It contains per-instance project copies populated from server state, is not managed by `recover`, and is not applied automatically as rollback.

Rollout Behavior
----------------

Targets can be supplied explicitly, selected interactively when no IDs are supplied, or discovered with `--all`. Automatic discovery accepts regular files named exactly `sync.<INSTANCE_ID>.json`, removes duplicate IDs, and processes the active instance first when it is included.

`rollout` validates each target's stored credentials, optionally creates the ZIP described above, and pushes targets sequentially while switching linked instances. The originally active instance is restored in a `finally` path, including after a failed push. With `--force`, validation, backup, or push failures are recorded while eligible targets continue; the command still finishes with a non-zero status when any failure was recorded. Rollout has no dry-run mode or automatic server rollback.

---

Canonical: https://www.zeyos.com/sdk/synchronization/zeysdk-compare

# Compare Files (zeysdk compare)

Compare Files (zeysdk compare)
==============================

The `compare` command compares local files with their server versions to detect content and property differences.

```bash
zeysdk compare [ASSET_PATH...] [options]
```

**Parameters**

| Parameter | Description | Required | Example |
|-----------|-------------|----------|---------|
| `ASSET_PATH` | Specific asset paths to compare | No | `services/backup.ixml weblets/dashboard.ixml` |

**Options**

| Option | Description |
|--------|-------------|
| `--verbose` | Show detailed output |

**Process**

1. **State Loading**
   - Reads local sync configuration
   - Loads local file metadata
   - Fetches matching server metadata

2. **Comparison**
   - Compares local file hashes with stored sync hashes
   - Compares server hashes with stored sync hashes
   - Checks metadata properties for differences

3. **Output**
   - Groups results by asset type
   - Marks local, server, and conflict states
   - Opens a diff tool when comparing selected files with content differences

**Status Indicators**

| Symbol | Meaning |
|--------|---------|
| `+` | Created locally or added on server |
| `-` | Deleted locally or deleted on server |
| `~` | Modified locally or modified on server |
| `!` | Conflict |
| `<>` | Property conflict |
| `->` | Local changes to be pushed |
| `<-` | Server changes to be pulled |

**Comparison Categories**

The output is organized by asset type:
- Settings
- Services
- Assets
- Resources
- Weblets

**Examples**

```bash
zeysdk compare
zeysdk compare services/backup.ixml weblets/dashboard.ixml
zeysdk compare --verbose
```

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Missing sync.json | No sync configuration | Run `zeysdk link` first |
| Invalid sync.json | Missing id/url | Re-link instance |
| Diff tool error | Tool launch failed | Select alternative tool |
| Server error | Cannot fetch remote | Check connection/auth |
| Parse error | Invalid file content | Check file format |

**Notes**

- `compare` does not change local files or server state
- `compare` reads the active instance and may fetch remote file contents to support a diff
- `compare` does not acquire `.zeysdk/.sync.lock`; avoid running it during a modifying synchronization command if you need a consistent snapshot
- Passing specific asset paths limits comparison and may open a diff workflow
- Metadata differences are reported separately from content changes
- Diff tool behavior depends on the global CLI configuration
- Remote snapshots reuse the content fetched during comparison, including empty files, and are removed after the comparison tool exits
- Beyond Compare uses its waiting launcher (`bcomp` on macOS, `BComp.com` on Windows), so an already-open window can read the remote snapshot before it is removed
- Beyond Compare's normal result codes (`0`, `1`, `2`, `11`–`14`) finish the comparison without prompting for another tool; launch failures and error codes remain failures
- To use [Hunk](https://www.hunk.dev), install its CLI separately, then select **Default Diff Tool → Hunk** in `zeysdk configure`; the viewer runs in the current terminal
- Custom comparison commands must wait until the viewer is finished before exiting

**Best Practices**

- Run `zeysdk compare` before `push`, `pull`, or `sync` when remote state may have changed
- Compare specific files when investigating a focused change
- Configure a diff tool with `zeysdk configure` for easier conflict review
- Resolve unexpected differences before running forced sync commands

---

Canonical: https://www.zeyos.com/sdk/synchronization/zeysdk-pull

# Pull Files

The `pull` command downloads server files to your local environment.

```bash
zeysdk pull [ASSET_PATH...] [options]
```

**Parameters**

| Parameter | Description | Required | Example |
|-----------|-------------|----------|---------|
| `ASSET_PATH` | Specific asset paths to pull | No | `services/backup.ixml` |

Instance-specific settings must match the active instance. For example, when linked to `dev`, `pull settings/settings.production.json` is rejected before requesting server data or changing project files. Select `settings/settings.dev.json` or switch the active instance first. The same rule applies to push, sync, and watch.

**Options**

| Option | Description |
|--------|-------------|
| `-f, --force` | Skip the confirmation before applying the pull |
| `--verbose` | Show detailed output |
| `--debug` | Show detailed debug information |
| `--debug-log` | Write debug output to `.zeysdk/debug.log` |
| `--delete-local-only` | Delete files that only exist locally |
| `--break-lock` | Replace the current sync lock after you verify its operation is no longer running |
| `--dry-run` | Read and report planned changes without applying them |
| `--concurrency <number>` | Maximum concurrent file operations (default: `10`) |
| `-q, --quiet` | Suppress progress indicators |

**Process**

1. **State Loading**
   - Reads `.zeysdk/sync.json`
   - Scans local files and metadata
   - Fetches server state for the active instance

2. **Change Detection**
   - Identifies files that differ from server state
   - Computes hashes for local files
   - Detects files that only exist locally when requested

3. **Pull Execution**
   - Downloads selected or detected server changes
   - Writes files to the local project
   - Updates local sync state after successful changes

4. **Safety Controls**
   - Acquires the project sync lock before scanning and releases its owner token on success, failure, or cancellation
   - Creates a rotating `sync.json` backup for a non-dry-run pull
   - Uses `--dry-run` to preview changes and `--force` to skip the apply confirmation

**Locking**

Pull uses `.zeysdk/.sync.lock`, shared with push, sync, rollout, and recover. If another operation owns the lock, pull exits without applying changes. A stale lock is reclaimed automatically. Use `--break-lock` only after confirming that the recorded process is no longer performing synchronization work; breaking a live lock can allow two destructive operations to overlap.

The lock is released from a `finally` path, including when the user declines the confirmation prompt.

**Dry Run**

`--dry-run` still scans local files, reads the active instance, and calculates the pull and deletion plan. It does not patch the server, write or delete project files, update the manifest or `sync.json`, or create a sync-state backup. When `--debug-log` is also supplied, the requested `.zeysdk/debug.log` is still written. Combining it with `--delete-local-only` reports which local-only files would be removed without deleting them.

**Sync-State Backup Scope**

A non-dry-run pull rotates `.zeysdk/sync.json.backup*` before applying its change set. If pull reports an error, it attempts to restore the newest backup. This recovery covers `sync.json` only: it cannot undo local files or manifest data already changed before a later failure. Use `zeysdk recover --list` to inspect retained backups.

**File Hashing and Synchronization**

The pull process determines which files need to be synchronized by comparing stored hashes with the server hashes returned by the API. Local files are hashed when their file objects are built; there is no timestamp or file-size pre-check before hashing.

The `lastmodified` value returned by the server is stored in `sync.json` for the synced item. It is not used to decide whether a file needs to be pulled.

Use `--verbose` to see detailed comparison and processing output.

**Examples**

```bash
zeysdk pull --verbose
zeysdk pull services/backup.ixml weblets/dashboard.ixml
zeysdk pull -f
zeysdk pull --dry-run
```

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Missing sync.json | No sync configuration | Run `zeysdk link` first |
| Invalid sync.json | Missing id/url | Re-link instance |
| Network error | Server unreachable | Check connection |
| Auth error | Invalid/expired token | Re-link application |
| Lock error | Sync lock already exists | Wait for current sync or use `--break-lock` |
| Write error | Cannot write local file | Check permissions |

**Warning**

Pull operations can overwrite local changes. Use `zeysdk compare` first to check for local modifications.

**Notes**

- Pull uses the currently active instance
- `--delete-local-only` removes files that exist locally but not on the server
- Local-only files are preserved unless `--delete-local-only` is present
- `--dry-run` reports planned changes without writing files, sync state, or manifest metadata
- Automatic recovery restores only `sync.json`, not project files already written or deleted
- Debug output is useful when investigating sync state or server responses

**Best Practices**

- Run `zeysdk compare` before pulling over local work
- Commit or snapshot important local changes before a broad pull
- Pull specific files when you only need selected server updates
- Use `--break-lock` only after confirming no other sync operation is still running

---

Canonical: https://www.zeyos.com/sdk/synchronization/zeysdk-push

# Push Files

The `push` command uploads local files to the ZeyOS server.

```bash
zeysdk push [ASSET_PATH...] [options]
```

**Parameters**

| Parameter | Description | Required | Example |
|-----------|-------------|----------|---------|
| `ASSET_PATH` | Specific asset paths to push | No | `services/backup.ixml` |

Instance-specific settings must match the active instance. For example, when linked to `dev`, `push settings/settings.production.json` is rejected before requesting server data or changing project files. Select `settings/settings.dev.json` or switch the active instance first.

**Options**

| Option | Description |
|--------|-------------|
| `-f, --force` | Skip the confirmation before applying the push. Does not change what the token is authorized to do |
| `--verbose` | Show detailed output |
| `--debug` | Show detailed debug information |
| `--debug-log` | Write debug output to `.zeysdk/debug.log` |
| `--break-lock` | Replace the current sync lock after you verify its operation is no longer running |
| `--dry-run` | Read and report planned server changes without applying them |
| `--keep-remote` | Preserve server-only assets that have no sync baseline in this checkout, instead of deleting them |
| `--concurrency <number>` | Maximum concurrent file operations (default: `10`) |
| `-q, --quiet` | Suppress progress indicators |

**Process**

1. **State Loading**
   - Reads `.zeysdk/sync.json`
   - Scans local files and manifest metadata
   - Fetches server state for comparison

2. **Change Detection**
   - Identifies files that changed locally
   - Detects local creates, deletes, and metadata updates
   - Marks services, weblets, resources, and assets that exist only on the server for deletion
   - Builds the server update payload

3. **Push Execution**
   - Uploads selected or detected local changes
   - Applies remote asset and metadata updates
   - Deletes server-only assets unless `--keep-remote` is supplied
   - Updates local sync state for the entries the server confirmed

4. **Safety Controls**
   - Acquires the project sync lock and releases its owner token on success, failure, or cancellation
   - Creates a rotating `sync.json` backup for a non-dry-run push
   - Uses `--dry-run` to preview server changes and `--force` to skip the apply confirmation

**Authoritative Push**

A whole-project push makes the server match the working copy. Any service, weblet, resource, or asset that exists on the server but not locally is deleted, whether or not this checkout has ever synchronized it. The preview lists each one as `delete` before the confirmation prompt, and the prompt states how many files will be removed.

Three things are never deleted this way:

- Settings slots (`settingscode`, `usersettingscode`, `defaultsettings`, `settings.&lt;instance&gt;.json`, `readme`) when the local project has no copy and no sync baseline for them. These are fixed server-side slots, so a missing local file means the project was never scaffolded with it rather than that the file was removed.
- Paths outside a targeted push (`zeysdk push services/backup.ixml`), which only touches the paths you name. A selected tracked file that has been removed locally can still be deleted on the server.
- Server-only assets with no sync baseline in this checkout, when `--keep-remote` is supplied.

`--keep-remote` does not protect every asset that is missing locally. If this checkout has a sync baseline for an asset and you remove it locally, a whole-project push still deletes it from the server, with or without `--keep-remote`.

Use `--dry-run` or `zeysdk compare` first when pushing to a shared instance, and `--keep-remote` when another developer's work may live on the same instance.

**Timeouts**

Requests time out after 30 seconds by default; set `ZEYSDK_REQUEST_TIMEOUT_MS` to change this. The request layer does not resend a push update automatically. `zeysdk watch` is the exception: it keeps a failed batch and pushes it again with backoff. After a timeout the outcome is unknown, because the server may have applied the change. Stop any running watcher and run `zeysdk compare` before pushing again. See [Verification Checkpoints](https://www.zeyos.com/ixml/ixml-guide/development-environment/verification-checkpoints).

**Locking**

Push uses `.zeysdk/.sync.lock`, shared with pull, sync, rollout, and recover. If another operation owns the lock, push exits without applying changes. A stale lock is reclaimed automatically. Use `--break-lock` only after confirming that the recorded process is no longer performing synchronization work.

The lock is released from a `finally` path, including when the user declines the confirmation prompt. Rollout holds this same project lock and passes its ownership into each nested push.

**Dry Run**

`--dry-run` still scans local files and reads the active instance. It reports uploads and server deletions without patching the server, cleaning missing entries out of `zeyos.app.json`, updating `sync.json`, or creating a sync-state backup. When `--debug-log` is also supplied, the requested `.zeysdk/debug.log` is still written.

During a normal push, manifest cleanup removes missing resource, service, and weblet entries before the change set is built. This cleanup is intentionally skipped for a dry run.

**Sync-State Backup Scope**

A non-dry-run push rotates `.zeysdk/sync.json.backup*` before applying its server change set. If push reports an error, it restores the pre-push manifest snapshot and attempts to restore the newest sync backup. It cannot reverse a server patch that succeeded before a later local update failed. Use `zeysdk recover --list` to inspect retained sync backups.

**File Hashing and Synchronization**

The push process determines which files need to be synchronized by comparing stored hashes with hashes calculated from the local files. There is no timestamp or file-size pre-check before hashing.

The `lastmodified` value returned by the server after a push is stored in `sync.json` for the synced item. It is not used to decide whether a file needs to be pushed.

Use `--verbose` to see detailed comparison and processing output.

**Examples**

```bash
zeysdk push --verbose
zeysdk push services/backup.ixml weblets/dashboard.ixml
zeysdk push -f
zeysdk push --dry-run
zeysdk push --keep-remote
```

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Missing sync.json | No sync configuration | Run `zeysdk link` first |
| Invalid sync.json | Missing id/url | Re-link instance |
| Network error | Server unreachable | Check connection |
| Auth error | Invalid/expired token | Re-link application |
| Lock error | Sync lock already exists | Wait for current sync or use `--break-lock` |
| File error | Cannot read local file | Check permissions |

**Notes**

- Push uses the currently active instance
- Push favors local content and can overwrite server-side changes; use `sync` when conflicts need explicit resolution
- A whole-project push deletes server-only assets; pass `--keep-remote` to preserve those with no sync baseline in this checkout
- A baseline is advanced only for entries the server echoed back in its PATCH response
- Successful server deletions are also removed from the corresponding local sync baseline
- `--dry-run` reports planned changes without modifying the server, manifest, or sync state
- Automatic recovery restores only `sync.json`; it is not a server rollback
- Debug output can be written to `.zeysdk/debug.log`

**Best Practices**

- Run `zeysdk compare` before pushing to shared or production instances
- Push specific files when only part of the app should change
- Use `--force` only when you have already reviewed the change set
- Confirm the active instance with `zeysdk whoami` before production pushes

---

Canonical: https://www.zeyos.com/sdk/synchronization/zeysdk-recover

# Recover Sync State

The `recover` command lists, restores, or deletes the rotating backups of `.zeysdk/sync.json` created by pull, push, and sync operations.

```bash
zeysdk recover [options]
```

**Options**

| Option | Description |
|--------|-------------|
| `--list` | List available backups, their indexes, paths, and relative ages |
| `--backup <index>` | Restore a backup index; `0` is the newest and is the default |
| `--clean` | Delete all rotating `sync.json` backups |
| `-f, --force` | Skip the restore or cleanup confirmation |

**Backup Files**

Before applying changes in a non-dry-run `pull`, `push`, or `sync`, the CLI rotates up to three sync-state backups:

```text
.zeysdk/sync.json.backup      # index 0, newest
.zeysdk/sync.json.backup.1    # index 1
.zeysdk/sync.json.backup.2    # index 2, oldest retained
```

The newest backup is retained after a successful operation and is also used for the automatic restore attempted when that operation reports a failure.

**Operations**

1. **List backups**

   ```bash
   zeysdk recover --list
   ```

   Listing does not change `sync.json` or the backups.

2. **Restore a backup**

   ```bash
   zeysdk recover
   zeysdk recover --backup 1
   zeysdk recover --backup 0 --force
   ```

   Restore overwrites the current `.zeysdk/sync.json`. Without `--force`, the command asks for confirmation and defaults to proceeding.

3. **Delete all backups**

   ```bash
   zeysdk recover --clean
   zeysdk recover --clean --force
   ```

   Without `--force`, cleanup asks for confirmation and defaults to cancellation.

Use one operation mode at a time. If options are combined, `--list` takes precedence over `--clean` and restore, while `--clean` takes precedence over restore.

**Locking**

Every recover mode, including `--list`, acquires `.zeysdk/.sync.lock` and releases it from a `finally` path. This prevents `sync.json` backup files from being listed, restored, or deleted while another synchronization command is rotating or updating them.

Recover does not expose `--break-lock`. Wait for the current operation to finish and retry. A stale lock is reclaimed automatically.

**Important Recovery Scope**

Recover restores only `.zeysdk/sync.json`. It does not restore:

- project source or asset files;
- `zeyos.app.json` manifest changes;
- remote server state or a server patch that already succeeded;
- rollout's timestamped ZIP archives under `.zeysdk/backups/`.

After recovering an older sync baseline, run `zeysdk compare` before the next modifying command. The recovered baseline may intentionally differ from both current local files and current server state.

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| No backups | No rotating sync-state backup exists | Run `--list`; recover files through version control or another backup if necessary |
| Invalid index | The complete value is not a non-negative decimal integer, or the index is unavailable | Use an index reported by `zeysdk recover --list` |
| Lock error | Another synchronization operation owns the project lock | Wait for it to finish and retry |
| Restore error | The selected backup cannot be copied over `sync.json` | Check file permissions and available disk space |

**Best Practices**

- Run `zeysdk recover --list` before restoring.
- Preserve or commit current work before replacing the sync baseline.
- Use `zeysdk compare` immediately after recovery.
- Treat rollout ZIP archives as manual snapshots, not as inputs to `zeysdk recover`.

---

Canonical: https://www.zeyos.com/sdk/synchronization/zeysdk-sync

# Sync Files (zeysdk sync)

Sync Files (zeysdk sync)
========================

The `sync` command synchronizes local files with the ZeyOS server and handles bidirectional changes.

```bash
zeysdk sync [ASSET_PATH...] [options]
```

**Parameters**

| Parameter | Description | Required | Example |
|-----------|-------------|----------|---------|
| `ASSET_PATH` | Specific asset paths to sync | No | `services/backup.ixml` |

**Options**

| Option | Description |
|--------|-------------|
| `-f, --force` | Skip the broad change-set confirmation; conflict choices may still prompt |
| `--verbose` | Show detailed output |
| `--debug` | Show detailed debug information |
| `--debug-log` | Write debug output to `.zeysdk/debug.log` |
| `--resolve <strategy>` | Conflict resolution strategy (`mine`, `theirs`, `merge`, or `skip`) |
| `--resolve-all` | Apply the `--resolve` strategy to every conflict instead of only the first |
| `--no-resolve` | Leave conflicts and their sync baselines untouched without prompting |
| `--break-lock` | Replace the current sync lock after you verify its operation is no longer running |
| `--dry-run` | Read and report planned changes without applying them |
| `--concurrency <number>` | Maximum concurrent file operations (default: `10`) |
| `-q, --quiet` | Suppress progress indicators |

**Process**

1. **Preparation**
   - Validates `.zeysdk/sync.json`
   - Removes stale SDK-owned merge temporary files
   - Acquires the project sync lock
   - For a non-dry-run operation, cleans missing manifest entries and creates a rotating `sync.json` backup

2. **State Collection**
   - Scans local files and settings
   - Fetches server state from the active instance
   - Computes local, sync, and server hashes

3. **Change Planning**
   - Builds local-to-server and server-to-local changes
   - Detects deletes and conflicts
   - Displays the planned synchronization result

4. **Conflict Handling**
   - Prompts for conflict resolution when needed
   - Applies `--resolve` to the first conflict, or to every conflict when combined with `--resolve-all`
   - Leaves skipped and unresolved conflicts, including their common sync baseline, untouched

5. **Apply Changes**
   - Pushes local changes to the server
   - Pulls server changes to local files
   - Updates sync state and manifest metadata
   - Releases the sync lock

**Locking**

Sync uses `.zeysdk/.sync.lock`, shared with pull, push, rollout, and recover. Lock creation is atomic and release is tied to the acquiring owner's token. If another operation owns the lock, sync exits without applying changes. A stale lock is reclaimed automatically.

Use `--break-lock` only after confirming that the recorded process is no longer performing synchronization work. The lock is released from a `finally` path when sync succeeds, fails, or the user cancels a prompt.

**Dry Run**

`--dry-run` scans local files, reads server state, computes changes, and reports conflicts. It returns before conflict resolution and before any apply step, even when `--resolve` is also supplied. It does not apply the planned server updates or deletions, change project files, clean or update `zeyos.app.json`, update `sync.json`, or create a sync-state backup. When `--debug-log` is also supplied, the requested `.zeysdk/debug.log` is still written.

The command may still remove an SDK-owned `zeysdk-sync-*` merge temporary file older than one hour from the system temporary directory.

**Status Indicators**

| Symbol | Meaning |
|--------|---------|
| `+` | Created locally or added on server |
| `-` | Deleted locally or deleted on server |
| `~` | Modified locally or modified on server |
| `!` | Conflict |
| `<>` | Property conflict |
| `->` | Local changes to be pushed |
| `<-` | Server changes to be pulled |

**Conflict Resolution**

When conflicts are detected, sync displays content and property differences and offers these strategies:

| Strategy | Result |
|----------|--------|
| `mine` | Use local content and metadata and send it to the server |
| `theirs` | Request server content and write it locally |
| `merge` | Open the configured comparison tool, then use the merged local file as the outgoing version |
| `skip` | Do not apply the conflict and do not advance its sync baseline |

`--resolve <strategy>` resolves only the first conflict and skips the remaining conflicts. Add `--resolve-all` to apply the same strategy to every conflict. `--resolve-all` has no effect by itself.

`--no-resolve` suppresses conflict prompts and leaves every conflict and its baseline untouched; non-conflicting changes can still be applied. `--force` skips the broad change-set confirmation but does not select a conflict strategy, so conflicts still prompt unless `--resolve` or `--no-resolve` is present.

**File Hashing and Synchronization**

The sync process determines which files need to be synchronized by comparing stored hashes with hashes calculated from local files and hashes returned by the server. There is no timestamp or file-size pre-check before hashing.

The `lastmodified` value returned by the server is stored in `sync.json` for the synced item. It is not used to decide whether a file needs to be synchronized.

Use `--verbose` to see detailed comparison and processing output.

**Examples**

```bash
zeysdk sync
zeysdk sync weblets/dashboard.ixml
zeysdk sync -f
zeysdk sync --dry-run
zeysdk sync --resolve mine --resolve-all
zeysdk sync --no-resolve
```

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Missing sync.json | No sync configuration | Run `zeysdk link` first |
| Invalid sync.json | Missing id/url | Re-link instance |
| Network error | Server unreachable | Check connection |
| Auth error | Invalid/expired token | Re-link application |
| Merge conflict | Unresolved changes | Choose a strategy, use `--no-resolve`, or rerun after manual reconciliation |
| Lock error | Sync lock already exists | Wait for current sync or use `--break-lock` |
| File system error | IO problems | Check permissions |

**Notes**

- `sync` is bidirectional and can change both local files and server state
- `--dry-run` previews planned operations without changing project, manifest, server, or sync state
- A non-dry-run sync creates a rotating backup of `sync.json` before applying changes
- On failure, automatic restore covers only `sync.json`; it cannot undo local, manifest, or server changes already applied
- Skipped and unresolved conflicts retain their previous sync baseline for a future reconciliation
- `lastmodified` is stored for synced items but is not used to decide whether files changed

**Best Practices**

- Use `zeysdk compare` before syncing when you expect conflicts
- Use `--dry-run` for production or scripted workflows
- Keep conflict resolution explicit with `--resolve` and `--resolve-all` when running non-interactively
- Use `zeysdk recover --list` to inspect retained sync-state backups after an interrupted operation
- Avoid `--break-lock` unless a previous sync was interrupted and no sync process is active

---

Canonical: https://www.zeyos.com/sdk/synchronization/zeysdk-watch

# Watch Files

The `watch` command monitors local files and automatically pushes updates to the ZeyOS server.

```bash
zeysdk watch [options]
```

**Options**

| Option | Description |
|--------|-------------|
| `--verbose` | Show detailed output |

**Watched Directories**

The following directories and files are monitored for changes:

- `settings` - Configuration files
- `services` - Service scripts
- `weblets` - Web components
- `resources` - Static resources
- `assets` - Application assets
- `zeyos.app.json` - Application manifest
- `README.md` - Application readme

**Process**

1. **Validation**
   - Validates active sync configuration
   - Checks each configured watch target before creating its watcher

2. **Watching**
   - Sets the interactive terminal title to `Watch: <app_identifier>` using the identifier from `zeyos.app.json`
   - Watches configured directories recursively
   - Skips missing targets at startup while a root watcher discovers recognized content directories created later
   - Ignores `.git`, `.zeysdk`, `node_modules`, editor temporary files, and unrelated project paths
   - Expands directory events to their relevant files and deduplicates changed paths during a 200 ms debounce window

3. **Auto Push**
   - Pushes one queued batch at a time
   - Queues changes received during an active push for a following batch
   - Uses force mode for automatic pushes and logs errors without stopping the watchers
   - Retains failed batches and retries them with bounded exponential backoff from 500 ms up to 30 seconds

**Locking and Concurrency**

Watch does not hold `.zeysdk/.sync.lock` continuously. Each automatic push acquires the same project lock used by pull, push, sync, rollout, and recover, then releases it when that batch finishes. This serializes batches within one watch process and prevents a batch from overlapping another lock-owning operation.

Watch has no `--break-lock` option. If a batch cannot acquire the lock or its push fails, the error is logged, the batch remains queued, and watching retries it with backoff. New changes join the retained batch. Run `zeysdk push` manually if you need to resolve the error outside watch mode.

**Automatic Push Scope**

Every batch is equivalent to a forced push of its changed paths: there is no confirmation and no dry-run mode. The server may be updated immediately. A non-dry-run push also creates a rotating `sync.json` backup, but that backup cannot undo a server update.

Removed files and directories are resolved against the manifest and sync baseline, and only those tracked paths are sent for deletion. Untracked removals are ignored; watch never escalates a removal into a whole-project push. Instance-specific settings files for another instance are rejected and excluded from the batch.

**Examples**

```bash
zeysdk watch
zeysdk watch --verbose
```

**Error Handling**

| Error | Description | Resolution |
|-------|-------------|------------|
| Missing sync.json | No sync configuration | Run `zeysdk link` first |
| Invalid sync.json | Missing id/url | Re-link instance |
| Watch error | Cannot monitor directory | Check permissions |
| Push error | Failed to upload changes | Check connection |
| Auth error | Invalid/expired token | Re-link application |
| Lock error | Another synchronization operation owns the lock | Wait for it to finish, then save again or run `zeysdk push` |

**Notes**

- Changes are pushed automatically
- Pushes run in force mode with a 200 ms debounce and are serialized within the watcher
- Failed batches remain queued and retry with bounded backoff up to 30 seconds
- Watch has no dry-run mode and does not hold the project lock between batches
- Missing targets are skipped at startup; the root watcher can discover recognized content directories created later
- Directory removals send only known descendants; unrelated remote-only files are preserved
- Stop watch with the normal process interrupt (for example, `Ctrl+C`)

**Best Practices**

- Use `watch` only when you intend local changes to reach the active instance immediately
- Confirm the active instance with `zeysdk whoami` before starting watch
- Avoid watch mode for production instances unless the workflow is intentional
- Stop and restart watch after changing linked instances or credentials

---

Canonical: https://www.zeyos.com/sdk/views

# ZeyOS Views Reference

Complete list of available views for weblet configuration (`--view` parameter).

## Main Views
- `main.index`
- `main.index-user`
- `main.index-group`
- `main.monitor`
- `main.monitor.export`
- `main.files`
- `main.files.export`

## Control Panel
- `control.index`
- `control.index.profile.form`
- `control.import`
- `control.servers`
- `control.servers.mailservers`
- `control.servers.mailservers.export`
- `control.servers.davservers`
- `control.servers.davservers.export`
- `control.servers.feedservers`
- `control.servers.feedservers.export`

### Mail Server Details
- `control.details_mailserver`
- `control.details_mailserver.head`
- `control.details_mailserver.masterdata`
- `control.details_mailserver.masterdata.form`
- `control.details_mailserver.form`

### DAV Server Details
- `control.details_davserver`
- `control.details_davserver.head`
- `control.details_davserver.masterdata`
- `control.details_davserver.masterdata.form`
- `control.details_davserver.form`

### Feed Server Details
- `control.details_feedserver`
- `control.details_feedserver.head`
- `control.details_feedserver.masterdata`
- `control.details_feedserver.masterdata.form`
- `control.details_feedserver.form`

## Messages
- `messages.index`
- `messages.index.list`
- `messages.index.table`
- `messages.index.export`
- `messages.details_message`
- `messages.details_message.head`
- `messages.details_message.masterdata`
- `messages.details_message.masterdata.form`
- `messages.details_message.newticket.form`
- `messages.details_message.form`
- `messages.details_message.attachments`

## Mailing Lists
- `mailinglists.index`
- `mailinglists.index.export`
- `mailinglists.details_mailinglist`
- `mailinglists.details_mailinglist.head`
- `mailinglists.details_mailinglist.masterdata`
- `mailinglists.details_mailinglist.masterdata.form`
- `mailinglists.details_mailinglist.newcampaign.form`
- `mailinglists.details_mailinglist.details`
- `mailinglists.details_mailinglist.details.form`
- `mailinglists.details_mailinglist.participants`
- `mailinglists.details_mailinglist.participants.export`
- `mailinglists.details_mailinglist.participants.addcontact.form`
- `mailinglists.details_mailinglist.participants.addparticipant.form`
- `mailinglists.participant.form`

## Contacts
- `contacts.index`
- `contacts.index.cards`
- `contacts.index.table`
- `contacts.index.export`
- `contacts.index.new.form`
- `contacts.details_contact`
- `contacts.details_contact.head`
- `contacts.details_contact.masterdata`
- `contacts.details_contact.masterdata.form`
- `contacts.details_contact.contactdata`
- `contacts.details_contact.contactdata.form`
- `contacts.details_contact.form`
- `contacts.details_contact.contacts.addnew.form`

## Calendar
- `calendar.index`
- `calendar.index.agenda`
- `calendar.index.day`
- `calendar.index.week`
- `calendar.index.month`
- `calendar.index.year`
- `calendar.appointments`
- `calendar.appointments.export`
- `calendar.appointments.new.form`
- `calendar.details_appointment`
- `calendar.details_appointment.head`
- `calendar.details_appointment.masterdata`
- `calendar.details_appointment.masterdata.form`
- `calendar.details_appointment.form`

## Projects
- `projects.index`
- `projects.index.export`
- `projects.index.new.form`
- `projects.details_project`
- `projects.details_project.head`
- `projects.details_project.masterdata`
- `projects.details_project.masterdata.form`
- `projects.details_project.form`
- `projects.details_project.tasks.addnew.form`
- `projects.details_project.tickets.addnew.form`

## Tasks
- `tasks.index`
- `tasks.index.export`
- `tasks.index.new.form`
- `tasks.details_task`
- `tasks.details_task.head`
- `tasks.details_task.masterdata`
- `tasks.details_task.masterdata.form`
- `tasks.details_task.form`
- `tasks.details_task.actionsteps.addnew.form`

## Action Steps
- `actionsteps.index`
- `actionsteps.index.export`
- `actionsteps.index.new.form`
- `actionsteps.details_actionstep`
- `actionsteps.details_actionstep.head`
- `actionsteps.details_actionstep.masterdata`
- `actionsteps.details_actionstep.masterdata.form`
- `actionsteps.details_actionstep.form`

## Notes
- `notes.index`
- `notes.index.export`
- `notes.index.new.form`
- `notes.details_note`
- `notes.details_note.head`
- `notes.details_note.masterdata`
- `notes.details_note.masterdata.form`
- `notes.details_note.form`
- `notes.details_note.attachments`

## Links
- `links.index`
- `links.index.export`
- `links.index.new.form`
- `links.details_link`
- `links.details_link.head`
- `links.details_link.masterdata`
- `links.details_link.masterdata.form`
- `links.details_link.form`

## Accounts
- `accounts.index`
- `accounts.index.cards`
- `accounts.index.table`
- `accounts.index.export`
- `accounts.details_account`
- `accounts.details_account.head`
- `accounts.details_account.masterdata`
- `accounts.details_account.masterdata.form`
- `accounts.details_account.newcontact.form`
- `accounts.details_account.contactdata`
- `accounts.details_account.details`
- `accounts.details_account.details.form`
- `accounts.details_account.contacts.addnew.form`
- `accounts.details_account.addresses`
- `accounts.details_account.addresses.export`
- `accounts.details_account.addresses.addnew.form`
- `accounts.details_account.items`
- `accounts.details_account.items.export`
- `accounts.details_account.prices`
- `accounts.details_account.prices.export`
- `accounts.details_account.cron`

## Billing
- `billing.index`
- `billing.index.export`
- `billing.index.quotes`
- `billing.index.quotes.export`
- `billing.index.orders`
- `billing.index.orders.export`
- `billing.index.deliveries`
- `billing.index.deliveries.export`
- `billing.index.invoices`
- `billing.index.invoices.export`
- `billing.index.credits`
- `billing.index.credits.export`
- `billing.details_transaction`
- `billing.details_transaction.head`
- `billing.details_transaction.masterdata`
- `billing.details_transaction.masterdata.form`
- `billing.details_transaction.addressdata`
- `billing.details_transaction.addressdata.form`
- `billing.details_transaction.form`
- `billing.details_transaction.items`
- `billing.details_transaction.payments.addnew.form`

## Procurement
- `procurement.index`
- `procurement.index.export`
- `procurement.index.orders`
- `procurement.index.orders.export`
- `procurement.index.deliveries`
- `procurement.index.deliveries.export`
- `procurement.index.invoices`
- `procurement.index.invoices.export`
- `procurement.index.credits`
- `procurement.index.credits.export`
- `procurement.details_transaction`
- `procurement.details_transaction.head`
- `procurement.details_transaction.masterdata`
- `procurement.details_transaction.masterdata.form`
- `procurement.details_transaction.addressdata`
- `procurement.details_transaction.addressdata.form`
- `procurement.details_transaction.form`
- `procurement.details_transaction.items`
- `procurement.details_transaction.payments.addnew.form`

## Collection
- `collection.index`
- `collection.index.receivables`
- `collection.index.receivables.export`
- `collection.index.payables`
- `collection.index.payables.export`
- `collection.index.dunning`
- `collection.index.dunning.export`
- `collection.details_dunning`
- `collection.details_dunning.head`
- `collection.details_dunning.masterdata`
- `collection.details_dunning.masterdata.form`
- `collection.details_dunning.addressdata`
- `collection.details_dunning.addressdata.form`
- `collection.details_dunning.form`
- `collection.details_dunning.transactions`

## Payments
- `payments.index`
- `payments.index.export`
- `payments.index.new.form`
- `payments.ledgers`
- `payments.ledgers.export`
- `payments.ledgers.new.form`
- `payments.details_payment`
- `payments.details_payment.head`
- `payments.details_payment.masterdata`
- `payments.details_payment.masterdata.form`
- `payments.details_payment.form`
- `payments.details_ledger`
- `payments.details_ledger.head`
- `payments.details_ledger.masterdata`
- `payments.details_ledger.masterdata.form`
- `payments.details_ledger.form`

## Contracts
- `contracts.index`
- `contracts.index.export`
- `contracts.index.new.form`
- `contracts.details_contract`
- `contracts.details_contract.head`
- `contracts.details_contract.masterdata`
- `contracts.details_contract.masterdata.form`
- `contracts.details_contract.details`
- `contracts.details_contract.details.form`
- `contracts.details_contract.billingitems`
- `contracts.details_contract.procurementitems`

## Tickets
- `tickets.index`
- `tickets.index.export`
- `tickets.index.new.form`
- `tickets.details_ticket`
- `tickets.details_ticket.head`
- `tickets.details_ticket.masterdata`
- `tickets.details_ticket.masterdata.form`
- `tickets.details_ticket.details`
- `tickets.details_ticket.details.form`
- `tickets.details_ticket.tasks.addnew.form`
- `tickets.details_ticket.actionsteps.addnew.form`
- `tickets.details_ticket.billingitems`
- `tickets.details_ticket.procurementitems`

## Opportunities
- `opportunities.index`
- `opportunities.index.export`
- `opportunities.index.new.form`
- `opportunities.details_opportunity`
- `opportunities.details_opportunity.head`
- `opportunities.details_opportunity.masterdata`
- `opportunities.details_opportunity.masterdata.form`
- `opportunities.details_opportunity.form`

## Campaigns
- `campaigns.index`
- `campaigns.index.export`
- `campaigns.index.new.form`
- `campaigns.details_campaign`
- `campaigns.details_campaign.head`
- `campaigns.details_campaign.masterdata`
- `campaigns.details_campaign.masterdata.form`
- `campaigns.details_campaign.details`
- `campaigns.details_campaign.details.form`
- `campaigns.details_campaign.participants`
- `campaigns.details_campaign.participants.export`
- `campaigns.details_campaign.participants.addcontact.form`
- `campaigns.details_campaign.participants.addparticipant.form`
- `campaigns.participant.form`

## Inventory
- `inventory.index`
- `inventory.index.export`
- `inventory.index.new.form`
- `inventory.storages`
- `inventory.storages.export`
- `inventory.storages.new.form`
- `inventory.devices`
- `inventory.devices.export`
- `inventory.transactions`
- `inventory.transactions.export`
- `inventory.details_item`
- `inventory.details_item.head`
- `inventory.details_item.masterdata`
- `inventory.details_item.masterdata.form`
- `inventory.details_item.details`
- `inventory.details_item.details.form`
- `inventory.details_item.inventory`
- `inventory.details_item.suppliers`
- `inventory.details_item.prices`
- `inventory.details_item.prices.export`
- `inventory.details_item.components`
- `inventory.details_item.components.export`
- `inventory.details_item.transactions`
- `inventory.details_item.transactions.export`
- `inventory.details_item.transactions.addtransaction.form`
- `inventory.details_item.transactions.addreservation.form`
- `inventory.details_item.transactions.transfer.form`
- `inventory.details_item.locations`
- `inventory.details_item.locations.export`
- `inventory.details_item.serials`
- `inventory.details_item.serials.export`
- `inventory.details_item.charges`
- `inventory.details_item.charges.export`
- `inventory.details_item.variants`
- `inventory.details_item.variants.export`
- `inventory.details_storage`
- `inventory.details_storage.head`
- `inventory.details_storage.masterdata`
- `inventory.details_storage.masterdata.form`
- `inventory.details_storage.details`
- `inventory.details_storage.details.form`
- `inventory.details_storage.locations`
- `inventory.details_storage.locations.export`
- `inventory.details_device`
- `inventory.details_device.head`
- `inventory.details_device.masterdata`
- `inventory.details_device.masterdata.form`
- `inventory.details_device.details`
- `inventory.details_device.details.form`
- `inventory.details_device.transactions`
- `inventory.details_device.transactions.export`
- `inventory.transaction.form`

## Price Lists
- `pricelists.index`
- `pricelists.index.pricelists`
- `pricelists.index.pricelists.export`
- `pricelists.index.pricelists.new.form`
- `pricelists.index.coupons`
- `pricelists.index.coupons.export`
- `pricelists.index.coupons.new.form`
- `pricelists.details_pricelist`
- `pricelists.details_pricelist.head`
- `pricelists.details_pricelist.masterdata`
- `pricelists.details_pricelist.masterdata.form`
- `pricelists.details_pricelist.details`
- `pricelists.details_pricelist.details.form`
- `pricelists.details_pricelist.prices`
- `pricelists.details_pricelist.prices.export`
- `pricelists.details_pricelist.accounts`
- `pricelists.details_pricelist.accounts.export`
- `pricelists.details_coupon`
- `pricelists.details_coupon.head`
- `pricelists.details_coupon.masterdata`
- `pricelists.details_coupon.masterdata.form`
- `pricelists.details_coupon.details`
- `pricelists.details_coupon.details.form`
- `pricelists.details_coupon.codes`
- `pricelists.details_coupon.codes.export`
- `pricelists.details_coupon.codes.add.form`
- `pricelists.details_coupon.code.form`

## Production
- `production.index`
- `production.index.export`
- `production.index.fabrications`
- `production.index.fabrications.export`
- `production.index.disassemblies`
- `production.index.disassemblies.export`
- `production.details_transaction`
- `production.details_transaction.head`
- `production.details_transaction.masterdata`
- `production.details_transaction.masterdata.form`
- `production.details_transaction.form`
- `production.details_transaction.items`
- `production.details_transaction.process.form`

## Administration
- `admin.index`
- `admin.index.users`
- `admin.index.users.export`
- `admin.index.users.new.form`
- `admin.index.groups`
- `admin.index.groups.export`
- `admin.index.groups.new.form`
- `admin.subscription`
- `admin.fields`
- `admin.fields.extdatafields`
- `admin.fields.tagfields`
- `admin.fields.taggroupfields`
- `admin.config`
- `admin.logs`
- `admin.details_user`
- `admin.details_user.head`
- `admin.details_user.masterdata`
- `admin.details_user.masterdata.form`
- `admin.details_user.newcontact.form`
- `admin.details_user.contactdata`
- `admin.details_user.form`
- `admin.details_group`
- `admin.details_group.head`
- `admin.details_group.masterdata`
- `admin.details_group.masterdata.form`
- `admin.details_group.form`
- `admin.details_application`
- `admin.details_application.head`
- `admin.details_application.masterdata`
- `admin.details_application.masterdata.form`
- `admin.details_application.form`
- `admin.details_service`
- `admin.details_service.head`
- `admin.details_service.masterdata`
- `admin.details_service.masterdata.form`
- `admin.details_service.form`
- `admin.details_resource`
- `admin.details_resource.head`
- `admin.details_resource.masterdata`
- `admin.details_resource.masterdata.form`
- `admin.details_resource.form`
- `admin.details_weblet`
- `admin.details_weblet.head`
- `admin.details_weblet.masterdata`
- `admin.details_weblet.masterdata.form`
- `admin.details_weblet.form`
- `admin.details_object`
- `admin.details_object.head`
- `admin.details_object.masterdata`
- `admin.details_object.masterdata.form`
- `admin.details_object.form`

## Development
- `dev.index`
- `dev.index.applications`
- `dev.index.applications.export`
- `dev.index.applications.new.form`
- `dev.index.services`
- `dev.index.services.export`
- `dev.index.resources`
- `dev.index.resources.export`
- `dev.index.weblets`
- `dev.index.weblets.export`
- `dev.objects`
- `dev.objects.export`
- `dev.objects.new.form`
- `dev.db`
- `dev.db.tables`
- `dev.db.tables.export`
- `dev.db.indexes`
- `dev.db.indexes.export`
- `dev.db.schemas`
- `dev.db.schemas.export`
- `dev.db.backends`
- `dev.db.backends.export`
- `dev.sql`
- `dev.sql.export`
- `dev.extdata`
- `dev.extdata.export`
- `dev.tags`
- `dev.tags.export`
