Skip to main content

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 and Metadata API read and write it:
  • xano workspace pull downloads the workspace as a multidoc and splits it into individual .xs files on your filesystem.
  • xano workspace 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.
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).

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:
A minimal multidoc (three definitions)
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. 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.
  • Apply it to an existing workspace (Metadata API) — send the multidoc to POST /workspace/{id}/multidoc.
Loan Origination App full workspace multidoc
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"
}

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:
ConstructKeywordDescription
Workspace SettingsworkspaceEnvironment variables, preferences, and configuration
Branch ConfigbranchBranch color, description, middleware, history retention
Database TablestableSchema definitions with fields, indexes, and views
API Groupsapi_groupAPI group settings (canonical URL, swagger, tags)
API EndpointsqueryHTTP endpoints with inputs, logic stacks, and responses
Custom FunctionsfunctionReusable logic blocks
Background TaskstaskScheduled and cron jobs
Triggerstable_trigger, realtime_trigger, workspace_trigger, agent_trigger, mcp_server_triggerEvent-driven handlers
MiddlewaremiddlewareRequest/response interceptors
AddonsaddonReusable subqueries for fetching related data
AI AgentsagentAI agent configuration
AI ToolstoolTools for agents and MCP servers
MCP Serversmcp_serverMCP server definitions
Teststest (embedded)Unit tests, defined inline within their parent construct

Optional inclusions

A few additional data types can be included via flags:
DataCLI FlagAPI ParameterDescription
Environment variables--envenv=trueCustom $env.* values defined in workspace settings
Table records--recordsrecords=trueActual data rows stored in each table
Draft function versions--draft (pull only)include_draft=trueUnpublished 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

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
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.

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.
GoalHow
Restore or clone to a fresh workspaceDashboard → Create Workspace (Multi-Doc)
Update an existing workspace, keep anything not in the docxano workspace push (default partial)
Make a workspace exactly match the multidocxano workspace push --sync --delete
Replace table data tooadd --records --truncate
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.

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
ParameterTypeInRequiredDefaultDescription
workspace_idintegerpathYesWorkspace ID
branchstringqueryNoLive branchBranch to retrieve
envbooleanqueryNofalseInclude environment variables
recordsbooleanqueryNofalseInclude table records
include_draftbooleanqueryNofalseInclude draft (unpublished) versions of functions
Response: The selected branch as a text/x-xanoscript multidoc.

Push a multidoc

POST /workspace/{workspace_id}/multidoc
ParameterTypeInRequiredDefaultDescription
workspace_idintegerpathYesWorkspace ID
branchstringqueryNoLive branchBranch to push to
partialbooleanqueryNofalseApply only the supplied definitions instead of a full replace (the CLI’s default push mode)
deletebooleanqueryNofalseRemove remote objects not present in the multidoc (full sync)
envbooleanqueryNofalseInclude environment variables
recordsbooleanqueryNofalseInclude table records
truncatebooleanqueryNofalseTruncate tables before importing records
as_draftbooleanqueryNofalseImport functions as draft versions instead of publishing
transactionbooleanqueryNotrueWrap the import in a database transaction
forcebooleanqueryNofalseSkip server-side safety checks (for CI/CD)
Request body: Content type text/x-xanoscript — the full multidoc as a string.
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.
Both endpoints require authentication via a Bearer token with appropriate workspace permissions. The same format powers several adjacent operations:
EndpointPurpose
GET / POST /sandbox/multidocExport / import the sandbox tenant
POST /workspace/{workspace_id}/release/multidocCreate a release from a multidoc
GET /workspace/{workspace_id}/release/{release_id}/multidocExport an existing release as a multidoc
GET /workspace/{workspace_id}/tenant/{tenant_name}/multidocExport 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.
  • 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.