> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xano.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Multidoc

> The single-document format representing one branch of a Xano workspace — what you and your AI agents pull, push, back up, and migrate

## What is a Multidoc?

A **multidoc** is a single XanoScript document that captures **one branch** of a workspace — every table, API, function, task, agent, and other construct — separated by `---` (triple-dash) delimiters. Think of it as a snapshot of a workspace branch in one file: like a database dump, but for your backend logic and schema.

You don't hand-author a multidoc — Xano generates it — but it's the format **you or your AI agents** work with for version control, backups, and migrating or cloning a workspace. The [CLI](/xano-cli/push-pull) and [Metadata API](/xano-features/metadata-api) read and write it:

* **[`xano workspace pull`](/xano-cli/push-pull#pull)** downloads the workspace as a multidoc and splits it into individual `.xs` files on your filesystem.
* **[`xano workspace push`](/xano-cli/push-pull#push)** reassembles your local `.xs` files into a multidoc and sends it to Xano.
* **The Metadata API** exposes `GET` and `POST` endpoints at `/workspace/{workspace_id}/multidoc` for the same purpose — this is what the CLI calls under the hood.

This page is the complete reference for that format — written so an agent (or a human) working through the CLI or Metadata API knows exactly what a multidoc contains and how it's applied, without needing any other context.

<Note>
  **A multidoc captures a single branch.** Pulling exports the live branch by default — pass a `branch` to target another. There is no "all branches" export, so back up each branch separately. By default a multidoc is **published definitions only**: table records, environment variables, and draft (unpublished) function versions are excluded unless you opt in (see [Optional inclusions](#optional-inclusions)).
</Note>

***

## Structure

A multidoc is a sequence of XanoScript definitions separated by `---`. Each section between separators is a standalone definition — a table, function, API endpoint, etc. — exactly as it would appear in its own `.xs` file. Here’s a minimal multidoc with three definitions:

```javascript lines icon="code" A minimal multidoc (three definitions) theme={null}
workspace "Loan Origination App" {
  preferences = {
    track_performance: true
    sql_columns      : true
  }
}
---
table loan_application {
  schema {
    int id
    int user_id {
      table = "user"
    }
    decimal amount
    text purpose
    enum status?=pending {
      values = ["pending", "approved", "rejected"]
    }
    timestamp created_at?=now
  }
}
---
query apply verb=POST {
  api_group = "Loan"
  auth = "user"

  input {
    decimal amount
    text purpose
  }

  stack {
    db.add loan_application {
      data = {
        user_id: $auth.id
        amount : $input.amount
        purpose: $input.purpose
        status : "pending"
      }
    } as $application
  }

  response = $application
}
```

The example above is trimmed for clarity. Below is a **real, complete multidoc** — the entire Loan Origination App workspace (3 tables, 2 API groups, and the full auth + loan flow) exported with [`xano workspace pull`](/xano-cli/push-pull#pull).

You can load a multidoc like this into Xano in a few ways:

* **Create a new workspace from it (dashboard)** — on the **Workspaces** page, open the **⋮** menu and choose **Create Workspace (Multi-Doc)**, then paste the multidoc (or click **Upload File**) and click **Create**.
* **Apply it to an existing workspace (CLI)** — assemble your `.xs` files and run [`xano workspace push`](/xano-cli/push-pull#push).
* **Apply it to an existing workspace (Metadata API)** — send the multidoc to `POST /workspace/{id}/multidoc`.

<Accordion title="Full example: the Loan Origination App workspace as a single multidoc" icon="files">
  ```javascript lines icon="code" Loan Origination App full workspace multidoc theme={null}
  workspace "Loan Origination App" {
    acceptance = {ai_terms: true}
    preferences = {
      internal_docs    : false
      track_performance: true
      sql_names        : false
      sql_columns      : true
    }
  }
  ---
  table user {
    auth = true

    schema {
      int id
      timestamp created_at?=now {
        visibility = "private"
      }
    
      text name filters=trim
      email? email filters=trim|lower
      password? password filters=min:8|minAlpha:1|minDigit:1
      enum role?=user {
        values = ["user", "admin"]
      }
    }

    index = [
      {type: "primary", field: [{name: "id"}]}
      {type: "btree", field: [{name: "created_at", op: "desc"}]}
      {type: "btree|unique", field: [{name: "email", op: "asc"}]}
      {type: "btree", field: [{name: "role"}]}
    ]

    guid = "0zDkg0JfwQkH9_AyPMmrsUAEqbg"
  }
  ---
  table loan {
    auth = false

    schema {
      int id
    
      // The application that originated this loan
      int application_id {
        table = "loan_application"
      }
    
      // The borrower
      int user_id {
        table = "user"
      }
    
      // The initial loan amount
      decimal amount
    
      // The current remaining balance
      decimal balance
    
      // The current status of the loan
      enum status?=active {
        values = ["active", "paid", "defaulted"]
      }
    
      timestamp created_at?=now
    }

    index = [
      {type: "primary", field: [{name: "id"}]}
      {type: "btree", field: [{name: "application_id"}]}
      {type: "btree", field: [{name: "user_id"}]}
      {type: "btree", field: [{name: "status"}]}
    ]

    guid = "GyMZM1p6PK7AmCHI-5T3QGnf160"
  }
  ---
  table loan_application {
    auth = false

    schema {
      int id
    
      // The user applying for the loan
      int user_id {
        table = "user"
      }
    
      // The requested loan amount
      decimal amount
    
      // The purpose of the loan
      text purpose
    
      // The current status of the application
      enum status?=pending {
        values = ["pending", "approved", "rejected"]
      }
    
      timestamp created_at?=now
    }

    index = [
      {type: "primary", field: [{name: "id"}]}
      {type: "btree", field: [{name: "user_id"}]}
      {type: "btree", field: [{name: "status"}]}
    ]

    guid = "g647JzT3IKgiZw8rJhrnL9d7Ajs"
  }
  ---
  api_group Authentication {
    canonical = "mIJgjWIF"
    guid = "cXyl6kSWyzgsUTihX5vdBLS1TH8"
  }
  ---
  // APIs for loan application and management
  api_group Loan {
    canonical = "loan-origination"
    guid = "Tvz9LM_yM8Vlpisy3AOfKC7to4M"
  }
  ---
  // Update application status (Admin only)
  // Approve or reject a loan application (Admin only)
  query "admin/application/{application_id}" verb=PATCH {
    api_group = "Loan"
    auth = "user"

    input {
      int application_id {
        table = "loan_application"
      }
    
      enum status {
        values = ["approved", "rejected"]
      }
    }

    stack {
      // 1. Get current user profile to check role
      db.get user {
        field_name = "id"
        field_value = $auth.id
      } as $user
    
      // 2. Verify admin role
      precondition ($user.role == "admin") {
        error_type = "accessdenied"
        error = "Admin privileges required"
      }
    
      // 3. Get the application
      db.get loan_application {
        field_name = "id"
        field_value = $input.application_id
      } as $application
    
      precondition ($application != null) {
        error_type = "notfound"
        error = "Application not found"
      }
    
      precondition ($application.status == "pending") {
        error = "Application has already been processed"
      }
    
      // 4. Update status and create loan in a transaction
      db.transaction {
        stack {
          // Update application status
          db.edit loan_application {
            field_name = "id"
            field_value = $input.application_id
            data = {status: $input.status}
          } as $updated_application
        
          // If approved, create the loan
          conditional {
            if ($input.status == "approved") {
              db.add loan {
                data = {
                  application_id: $application.id
                  user_id       : $application.user_id
                  amount        : $application.amount
                  balance       : $application.amount
                  status        : "active"
                }
              } as $new_loan
            }
          }
        }
      }
    }

    response = {
      message       : "Application "|concat:$input.status
      application_id: $input.application_id
    }

    guid = "DdHvEGUKVQfwC-h9tPiggfMZGVE"
  }
  ---
  // List all applications (Admin only)
  // List all loan applications (Admin only)
  query "admin/applications" verb=GET {
    api_group = "Loan"
    auth = "user"

    input {
      int page?=1
      int per_page?=20
      enum status? {
        values = ["pending", "approved", "rejected"]
      }
    }

    stack {
      // 1. Get current user profile to check role
      db.get user {
        field_name = "id"
        field_value = $auth.id
      } as $user
    
      // 2. Verify admin role
      precondition ($user.role == "admin") {
        error_type = "accessdenied"
        error = "Admin privileges required"
      }
    
      // 3. Query all applications
      db.query loan_application {
        where = $db.loan_application.status ==? $input.status
        sort = {created_at: "desc"}
        return = {
          type  : "list"
          paging: {page: $input.page, per_page: $input.per_page}
        }
      } as $applications
    }

    response = $applications
    guid = "zfkPgd8FET1-te_Q8itQsSzAuIY"
  }
  ---
  // Create a new loan application
  // Submit a new loan application
  query apply verb=POST {
    api_group = "Loan"
    auth = "user"

    input {
      // The requested loan amount
      decimal amount
    
      // The purpose of the loan
      text purpose
    }

    stack {
      // Add the application to the database
      db.add loan_application {
        data = {
          user_id: $auth.id
          amount : $input.amount
          purpose: $input.purpose
          status : "pending"
        }
      } as $application
    }

    response = $application
    guid = "_IbZxk9A1rnaI0adELHOto3l8N0"
  }
  ---
  // Login and retrieve an authentication token
  query "auth/login" verb=POST {
    api_group = "Authentication"

    input {
      email email? filters=trim|lower
      text password?
    }

    stack {
      db.get user {
        field_name = "email"
        field_value = $input.email
        output = ["id", "created_at", "name", "email", "password"]
      } as $user
    
      precondition ($user != null) {
        error_type = "accessdenied"
        error = "Invalid Credentials."
      }
    
      security.check_password {
        text_password = $input.password
        hash_password = $user.password
      } as $pass_result
    
      precondition ($pass_result) {
        error_type = "accessdenied"
        error = "Invalid Credentials."
      }
    
      security.create_auth_token {
        table = "user"
        extras = {}
        expiration = 86400
        id = $user.id
      } as $authToken
    }

    response = {authToken: $authToken}
    guid = "wiLiUSYLcPkhUCY1xDYlEpKGDIE"
  }
  ---
  // Get the user record belonging to the authentication token
  query "auth/me" verb=GET {
    api_group = "Authentication"
    auth = "user"

    input {
    }

    stack {
      db.get user {
        field_name = "id"
        field_value = $auth.id
        output = ["id", "created_at", "name", "email"]
      } as $user
    }

    response = $user
    guid = "Q7UKZk3KmMLn7R903IPVWaYc2BI"
  }
  ---
  // Signup and retrieve an authentication token
  query "auth/signup" verb=POST {
    api_group = "Authentication"

    input {
      text name?
      email email? filters=trim|lower
      text password?
    }

    stack {
      db.get user {
        field_name = "email"
        field_value = $input.email
      } as $user
    
      precondition ($user == null) {
        error_type = "accessdenied"
        error = "This account is already in use."
      }
    
      db.add user {
        enforce_hidden_fields = false
        data = {
          created_at: "now"
          name      : $input.name
          email     : $input.email
          password  : $input.password
        }
      } as $user
    
      security.create_auth_token {
        table = "user"
        extras = {}
        expiration = 86400
        id = $user.id
      } as $authToken
    }

    response = {authToken: $authToken}
    guid = "5_4yZbXcseUitQREfwiFIhv6bcA"
  }
  ---
  // List applications for the authenticated user
  // List all applications for the current user
  query my_applications verb=GET {
    api_group = "Loan"
    auth = "user"

    input {
      int page?=1
      int per_page?=20
    }

    stack {
      // Query the loan_application table for the current user
      db.query loan_application {
        where = $db.loan_application.user_id == $auth.id
        sort = {created_at: "desc"}
        return = {
          type  : "list"
          paging: {page: $input.page, per_page: $input.per_page}
        }
      } as $applications
    }

    response = $applications
    guid = "Rw6xcu6c0nh7iVglzD0l4e-crEo"
  }
  ---
  // List active loans for the authenticated user
  // List all active loans for the current user
  query my_loans verb=GET {
    api_group = "Loan"
    auth = "user"

    input {
      int page?=1
      int per_page?=20
    }

    stack {
      // Query the loan table for the current user
      db.query loan {
        where = $db.loan.user_id == $auth.id
        sort = {created_at: "desc"}
        return = {
          type  : "list"
          paging: {page: $input.page, per_page: $input.per_page}
        }
      } as $loans
    }

    response = $loans
    guid = "Tfrfukm3jbEJkE_9AeHlsMI5UzE"
  }
  ```
</Accordion>

### Key rules

* Each definition is separated by exactly `---` on its own line
* Every `.xs` file contributes one definition to the multidoc
* Definitions can appear in any order, though the CLI sorts them alphabetically by file path when assembling a push
* The CLI's default push is *partial* — only changed definitions are sent. Use `--sync` for a full push of every definition, and `--sync --delete` to also remove remote objects that are no longer present locally
* Each definition must be a valid, standalone XanoScript primitive

***

## What's Included

A multidoc can contain every type of XanoScript construct in your workspace. Here's the full list:

| Construct                                            | Keyword                                                                                         | Description                                              |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| [Workspace Settings](/xanoscript/workspace-settings) | `workspace`                                                                                     | Environment variables, preferences, and configuration    |
| [Branch Config](/xanoscript/workspace-settings)      | `branch`                                                                                        | Branch color, description, middleware, history retention |
| [Database Tables](/xanoscript/db)                    | `table`                                                                                         | Schema definitions with fields, indexes, and views       |
| [API Groups](/xanoscript/api)                        | `api_group`                                                                                     | API group settings (canonical URL, swagger, tags)        |
| [API Endpoints](/xanoscript/api)                     | `query`                                                                                         | HTTP endpoints with inputs, logic stacks, and responses  |
| [Custom Functions](/xanoscript/custom-functions)     | `function`                                                                                      | Reusable logic blocks                                    |
| [Background Tasks](/xanoscript/tasks)                | `task`                                                                                          | Scheduled and cron jobs                                  |
| [Triggers](/xanoscript/triggers)                     | `table_trigger`, `realtime_trigger`, `workspace_trigger`, `agent_trigger`, `mcp_server_trigger` | Event-driven handlers                                    |
| [Middleware](/xanoscript/middleware)                 | `middleware`                                                                                    | Request/response interceptors                            |
| [Addons](/xanoscript/addons)                         | `addon`                                                                                         | Reusable subqueries for fetching related data            |
| [AI Agents](/xanoscript/agents)                      | `agent`                                                                                         | AI agent configuration                                   |
| [AI Tools](/xanoscript/ai-tools)                     | `tool`                                                                                          | Tools for agents and MCP servers                         |
| [MCP Servers](/xanoscript/mcp-servers)               | `mcp_server`                                                                                    | MCP server definitions                                   |
| [Tests](/xanoscript/tests)                           | `test` (embedded)                                                                               | Unit tests, defined inline within their parent construct |

### Optional inclusions

A few additional data types can be included via flags:

| Data                    | CLI Flag              | API Parameter        | Description                                          |
| ----------------------- | --------------------- | -------------------- | ---------------------------------------------------- |
| Environment variables   | `--env`               | `env=true`           | Custom `$env.*` values defined in workspace settings |
| Table records           | `--records`           | `records=true`       | Actual data rows stored in each table                |
| Draft function versions | `--draft` (pull only) | `include_draft=true` | Unpublished draft versions of functions              |

These are excluded by default since they contain runtime data rather than workspace definitions.

***

## How the CLI Uses Multidoc

The CLI is the primary interface for working with multidoc. Understanding how it transforms between multidoc and individual files helps when troubleshooting push/pull issues.

### Pull: Multidoc to files

When you run `xano workspace pull`, the CLI:

1. Calls `GET /workspace/{workspace_id}/multidoc` on the Metadata API
2. Receives a single multidoc response
3. Splits it on `---` boundaries
4. Writes each definition to its own `.xs` file, organized by type into the [directory structure](/xano-cli/push-pull#pull)

### Push: Files to multidoc

When you run `xano workspace push`, the CLI:

1. Recursively collects all `.xs` files from the target directory
2. Sorts them alphabetically by file path
3. Joins them with `---` separators into a single multidoc
4. Sends it to `POST /workspace/{workspace_id}/multidoc` with content type `text/x-xanoscript`

<Note>
  The order of definitions within the multidoc doesn't matter to Xano — the server resolves dependencies regardless of order. The CLI sorts alphabetically for consistency and readable diffs.
</Note>

***

## Importing and overwrite behavior

Multidocs are how you (or an agent) back up, restore, clone, and migrate a workspace — so it matters where you send one and what it overwrites:

* **Import as a new workspace (dashboard)** — on the **Workspaces** page, the **⋮** menu → **Create Workspace (Multi-Doc)** builds a brand-new workspace from the multidoc. Nothing is overwritten — the safest path for restoring a backup elsewhere, cloning, or migrating between instances.
* **Push into an existing workspace** — Xano matches each definition to existing objects by its `guid`: matches are **updated in place** (overwritten) and new definitions are created. Objects already in the workspace but **absent** from the multidoc are left untouched — *unless* you use `--sync --delete` (`delete=true`), which removes them so the workspace matches the multidoc exactly. That delete is destructive.
* **Records** are only written with `--records`/`records=true`. By default rows are added on top of existing data (which can duplicate); add `--truncate`/`truncate=true` to empty each table first so the import replaces it.
* Pushes run inside a database transaction by default (`transaction=true`), so a failed import rolls back instead of leaving the workspace half-applied.

| Goal                                                       | How                                          |
| ---------------------------------------------------------- | -------------------------------------------- |
| Restore or clone to a fresh workspace                      | Dashboard → **Create Workspace (Multi-Doc)** |
| Update an existing workspace, keep anything not in the doc | `xano workspace push` (default partial)      |
| Make a workspace **exactly** match the multidoc            | `xano workspace push --sync --delete`        |
| Replace table data too                                     | add `--records --truncate`                   |

<Note>
  Every definition carries a `guid` (visible in the example above). That GUID is how Xano decides whether a push **updates** an existing object or **creates** a new one — which is why importing the same multidoc twice updates in place instead of duplicating.
</Note>

***

## Metadata API Endpoints

The Metadata API provides direct access to the multidoc format for programmatic workflows, CI/CD pipelines, or custom tooling.

### Retrieve a multidoc

```
GET /workspace/{workspace_id}/multidoc
```

| Parameter       | Type    | In    | Required | Default     | Description                                       |
| --------------- | ------- | ----- | -------- | ----------- | ------------------------------------------------- |
| `workspace_id`  | integer | path  | Yes      | —           | Workspace ID                                      |
| `branch`        | string  | query | No       | Live branch | Branch to retrieve                                |
| `env`           | boolean | query | No       | `false`     | Include environment variables                     |
| `records`       | boolean | query | No       | `false`     | Include table records                             |
| `include_draft` | boolean | query | No       | `false`     | Include draft (unpublished) versions of functions |

**Response:** The selected branch as a `text/x-xanoscript` multidoc.

### Push a multidoc

```
POST /workspace/{workspace_id}/multidoc
```

| Parameter      | Type    | In    | Required | Default     | Description                                                                                 |
| -------------- | ------- | ----- | -------- | ----------- | ------------------------------------------------------------------------------------------- |
| `workspace_id` | integer | path  | Yes      | —           | Workspace ID                                                                                |
| `branch`       | string  | query | No       | Live branch | Branch to push to                                                                           |
| `partial`      | boolean | query | No       | `false`     | Apply only the supplied definitions instead of a full replace (the CLI's default push mode) |
| `delete`       | boolean | query | No       | `false`     | Remove remote objects not present in the multidoc (full sync)                               |
| `env`          | boolean | query | No       | `false`     | Include environment variables                                                               |
| `records`      | boolean | query | No       | `false`     | Include table records                                                                       |
| `truncate`     | boolean | query | No       | `false`     | Truncate tables before importing records                                                    |
| `as_draft`     | boolean | query | No       | `false`     | Import functions as draft versions instead of publishing                                    |
| `transaction`  | boolean | query | No       | `true`      | Wrap the import in a database transaction                                                   |
| `force`        | boolean | query | No       | `false`     | Skip server-side safety checks (for CI/CD)                                                  |

**Request body:** Content type `text/x-xanoscript` — the full multidoc as a string.

<Note>
  To preview a push without applying it, send the same request to **`POST /workspace/{workspace_id}/multidoc/dry-run`**. This is what backs the CLI's `--dry-run` flag.
</Note>

Both endpoints require authentication via a Bearer token with appropriate workspace permissions.

### Related multidoc endpoints

The same format powers several adjacent operations:

| Endpoint                                                      | Purpose                                                 |
| ------------------------------------------------------------- | ------------------------------------------------------- |
| `GET` / `POST /sandbox/multidoc`                              | Export / import the [sandbox](/xano-cli/sandbox) tenant |
| `POST /workspace/{workspace_id}/release/multidoc`             | Create a [release](/xano-cli/releases) from a multidoc  |
| `GET /workspace/{workspace_id}/release/{release_id}/multidoc` | Export an existing release as a multidoc                |
| `GET /workspace/{workspace_id}/tenant/{tenant_name}/multidoc` | Export a specific tenant's workspace                    |

***

## When you'll work with multidoc

Whether you're an AI agent operating through the CLI/Metadata API or a human reviewing a diff, multidoc is the format under the hood:

* **Version control** — A pulled workspace is a tree of `.xs` files (one definition each) that reassemble into a multidoc on push, so changes diff and review like any other code.
* **Backup & restore** — Export a branch to a multidoc, then re-import it as a new workspace or push it back to restore. See [Importing and overwrite behavior](#importing-and-overwrite-behavior).
* **Migration & cloning** — Move a workspace between branches or instances by exporting one multidoc and importing it elsewhere.
* **CI/CD pipelines** — Automated deployments send and receive multidoc payloads directly against the Metadata API.
* **Debugging push failures** — Knowing the CLI assembles your `.xs` files into one multidoc helps you isolate which definition broke a push.
* **Custom tooling** — If you're building tools that interact with Xano programmatically, the multidoc endpoints are the transport layer for reading and writing workspace definitions.
