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

# Data model reference

> The objects the ModuleX API and SDKs return — organizations, users and keys, workflows and runs, credentials, knowledge bases, and the wallet — their fields, how they relate, and the identifier conventions you need to join data correctly.

export const MediaEmbed = ({id, type = 'screenshot', caption = '', ext, ratio = '16 / 9'}) => {
  const isVideo = type === 'video' || type === 'app_video';
  const resolvedExt = ext || (isVideo ? 'mp4' : type === 'screenshot' ? 'webp' : 'svg');
  const src = 'https://media.modulex.dev/' + id + '.' + resolvedExt;
  const [status, setStatus] = useState('loading');
  const [isDev, setIsDev] = useState(false);
  const [inView, setInView] = useState(false);
  const boxRef = useRef(null);
  useEffect(() => {
    if (typeof window === 'undefined') return;
    const h = window.location.hostname;
    setIsDev(h === 'localhost' || h === '127.0.0.1' || h.endsWith('.mintlify.app'));
  }, []);
  useEffect(() => {
    if (inView) return;
    if (typeof IntersectionObserver === 'undefined') {
      setInView(true);
      return;
    }
    const el = boxRef.current;
    if (!el) return;
    const io = new IntersectionObserver(entries => {
      if (entries.some(e => e.isIntersecting)) {
        setInView(true);
        io.disconnect();
      }
    }, {
      rootMargin: '300px'
    });
    io.observe(el);
    return () => io.disconnect();
  }, [inView]);
  if (status === 'missing') {
    if (!isDev) return null;
    return <div style={{
      display: 'flex',
      flexDirection: 'column',
      justifyContent: 'center',
      gap: '0.4rem',
      padding: '1rem 1.25rem',
      margin: '1.25rem 0',
      width: '100%',
      aspectRatio: ratio,
      boxSizing: 'border-box',
      border: '1px dashed rgba(128,128,128,0.45)',
      borderRadius: '0.75rem',
      background: 'rgba(128,128,128,0.06)',
      color: 'currentColor',
      fontSize: '0.85rem',
      lineHeight: 1.45
    }}>
        <div style={{
      display: 'flex',
      alignItems: 'center',
      gap: '0.5rem',
      opacity: 0.75
    }}>
          <span aria-hidden="true">🎬</span>
          <code style={{
      fontSize: '0.75rem'
    }}>{id}</code>
          <span style={{
      fontSize: '0.65rem',
      textTransform: 'uppercase',
      letterSpacing: '0.04em',
      padding: '0.1rem 0.4rem',
      borderRadius: '0.4rem',
      background: 'rgba(128,128,128,0.18)'
    }}>
            {type}
          </span>
        </div>
        <div style={{
      opacity: 0.9
    }}>{caption || 'Media not uploaded yet.'}</div>
        <div style={{
      fontSize: '0.7rem',
      opacity: 0.5
    }}>
          Upload to R2 as <code>{id}.{resolvedExt}</code> — preview only, hidden in production.
        </div>
      </div>;
  }
  const mediaStyle = {
    display: status === 'loaded' ? 'block' : 'none',
    width: '100%',
    height: 'auto',
    borderRadius: '0.75rem'
  };
  const media = isVideo ? <video src={inView ? src : undefined} autoPlay loop muted playsInline preload="metadata" onLoadedData={() => setStatus('loaded')} onError={() => setStatus('missing')} style={mediaStyle} /> : <img src={inView ? src : undefined} alt={caption} onLoad={() => setStatus('loaded')} onError={() => setStatus('missing')} style={mediaStyle} />;
  return <figure style={{
    margin: '1.25rem 0'
  }}>
      <div ref={boxRef} style={status === 'loaded' ? {
    width: '100%'
  } : {
    width: '100%',
    aspectRatio: ratio,
    borderRadius: '0.75rem',
    background: 'rgba(128,128,128,0.06)'
  }}>
        {media}
      </div>
      {status === 'loaded' && caption ? <figcaption style={{
    marginTop: '0.5rem',
    textAlign: 'center',
    fontSize: '0.85rem',
    opacity: 0.7
  }}>
          {caption}
        </figcaption> : null}
    </figure>;
};

This page is the reference for the objects you meet through the ModuleX API and SDKs:
what each one is, the fields it returns, how the objects relate, and the identifier
conventions you need to join them correctly. It describes the data model **as it is
exposed on the API** — internal storage, indexing, and database tuning are out of scope.

<Note>
  Wire responses are **snake\_case** JSON. The official SDKs convert between camelCase and
  snake\_case at their own boundary, so a field shown here as `edit_version` surfaces as
  `editVersion` in the JavaScript SDK and stays `edit_version` in the Python SDK and raw
  REST. See `/sdks/parity`.
</Note>

## Entity map

Two objects sit at the centre of the model — the **organization** and the **user**. An
organization is the tenant and billing unit; a user is a person. Every workflow, run,
credential, knowledge base, chat, and wallet belongs to exactly one organization.

Tenant isolation is enforced per request: you select the active organization with the
`X-Organization-ID` header, and every response is scoped to it. See `/security/org-context`.

<Frame caption="Core objects and how they relate">
  ```mermaid theme={null}
  erDiagram
      Organization ||--o{ ApiKey : "issues"
      Organization ||--o{ Credential : "owns"
      Organization ||--o{ Workflow : "owns"
      Organization ||--o{ Run : "owns"
      Organization ||--o{ KnowledgeBase : "owns"
      Organization ||--|| Wallet : "has one"
      Organization ||--o{ Member : "has"

      User ||--o{ Member : "belongs via"
      User ||--o{ ApiKey : "creates"

      Workflow ||--o{ Deployment : "is deployed as"
      Workflow ||--o{ EditHistory : "tracks edits in"
      Workflow ||--o{ Schedule : "is scheduled by"
      Workflow ||--o{ Run : "executes as"

      Credential ||--o| KnowledgeBase : "backs (native KB)"

      KnowledgeBase ||--o{ Document : "contains"
      Document ||--o{ Chunk : "is split into"

      Chat ||--o{ Run : "triggers"
  ```
</Frame>

<MediaEmbed id="MX-MEDIA-4410" type="image" caption={"A polished entity-relationship diagram of the ModuleX core data model."} />

## Conventions you must know first

<AccordionGroup>
  <Accordion title="Timestamps are UTC" icon="clock">
    Treat every ModuleX timestamp as UTC and normalize on your side. A timezone-aware value
    emits an offset on the wire; a naive one does not — so normalize rather than comparing the
    raw strings.
  </Accordion>

  <Accordion title="Metadata fields use prefixed names" icon="brackets-curly">
    Free-form metadata surfaces under prefixed field names rather than a bare `metadata` key:
    `doc_metadata` on documents, `chunk_metadata` on chunks, and `extra_metadata` on wallet
    ledger entries and catalog entries.
  </Accordion>

  <Accordion title="Reference credentials by credential_id" icon="key">
    A credential is referenced everywhere — in the API, the SDKs, and from a native knowledge
    base — by its **`credential_id`**, a unique UUID. Always join on `credential_id`.
  </Accordion>
</AccordionGroup>

## Organizations, users, and access

<ResponseField name="organization" type="object">
  The tenant and billing unit. Every managed-usage record, plan, wallet, workflow, and
  knowledge base is scoped to one organization. Selected per request by the
  `X-Organization-ID` header. See `/concepts/organizations-roles`.

  <Expandable title="key fields">
    <ResponseField name="id" type="string (uuid)" required>The organization id.</ResponseField>
    <ResponseField name="slug" type="string" required>Unique URL-safe identifier.</ResponseField>
    <ResponseField name="name" type="string" required>Display name.</ResponseField>
    <ResponseField name="stripe_customer_id" type="string | null">The linked Stripe customer, set once the org has a billing relationship.</ResponseField>
    <ResponseField name="settings" type="object">Free-form organization settings.</ResponseField>
    <ResponseField name="has_ever_trialed" type="boolean">Trial-eligibility flag; never cleared once set.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="membership" type="object">
  The join between a user and an organization, carrying the user's role.

  <Expandable title="key fields">
    <ResponseField name="user_id" type="string (uuid)" required>The member.</ResponseField>
    <ResponseField name="organization_id" type="string (uuid)" required>The organization.</ResponseField>
    <ResponseField name="role" type="string" required>The live roles are **`owner`** and **`admin`** only.</ResponseField>
  </Expandable>
</ResponseField>

<Warning>
  The **`member` role is retired** (deprecated 2026-06-20). It is no longer a current
  first-class role: Composer, Assistant, Knowledge, and schedule operations require
  `owner` or `admin`. Treat only `owner` and `admin` as valid roles. See
  `/security/roles-permissions`.
</Warning>

<ResponseField name="api_key" type="object">
  An API key with the prefix `mx_live_`. The plaintext key is shown **once** at creation;
  afterwards only a hint is kept for display, and the key itself is never returned again.

  <Expandable title="key fields">
    <ResponseField name="id" type="string (uuid)" required>The key id.</ResponseField>
    <ResponseField name="organization_id" type="string (uuid) | null">The org the key is scoped to. Null means the key works across all of the creating user's organizations — in which case you must still pass `X-Organization-ID` to choose the active org per request.</ResponseField>
    <ResponseField name="name" type="string" required>A label for the key.</ResponseField>
    <ResponseField name="key_hint" type="string">The first few characters of the key, for display.</ResponseField>
    <ResponseField name="rate_limit_per_minute" type="integer">Per-key request limit. Default `60`.</ResponseField>
    <ResponseField name="is_active" type="boolean">Whether the key is usable.</ResponseField>
    <ResponseField name="expires_at" type="string (timestamp) | null">Optional expiry.</ResponseField>
    <ResponseField name="last_used_at" type="string (timestamp) | null">Last successful use.</ResponseField>
  </Expandable>
</ResponseField>

Send the key as `Authorization: Bearer mx_live_…` together with `X-Organization-ID`. The
backend also accepts `X-API-KEY: mx_live_…`. See `/api-reference/authentication`.

## Workflows

A **workflow** is the editable graph — nodes, edges, and run state. A **run** is one
execution of a workflow. The two are distinct objects; see
`/concepts/workflows-and-runs` for the conceptual model and `/concepts/workflow-engine`
for how the graph executes.

<ResponseField name="workflow" type="object">
  The durable, editable workflow record.

  <Expandable title="key fields">
    <ResponseField name="id" type="string (uuid)" required>The workflow id.</ResponseField>
    <ResponseField name="organization_id" type="string (uuid)" required>Owning org.</ResponseField>
    <ResponseField name="creator_id" type="string (uuid) | null">User who created it; nulled if that user is deleted.</ResponseField>
    <ResponseField name="name" type="string" required>Display name.</ResponseField>
    <ResponseField name="description" type="string | null">Description.</ResponseField>
    <ResponseField name="version" type="string">Semantic version string. Default `"1.0.0"`. This is a label, distinct from `edit_version` below.</ResponseField>
    <ResponseField name="tags" type="array">Tag list.</ResponseField>
    <ResponseField name="status" type="string">One of `draft`, `active`, `archived`. Default `draft`.</ResponseField>
    <ResponseField name="visibility" type="string">One of `private`, `organization`, `public`, `system`. Default `organization`.</ResponseField>
    <ResponseField name="workflow_schema" type="object" required>The complete workflow definition — see below.</ResponseField>
    <ResponseField name="input" type="object">Default run input.</ResponseField>
    <ResponseField name="config" type="object">Run configuration.</ResponseField>
    <ResponseField name="edit_version" type="integer" required>A monotonically increasing edit counter. Default `0`. Distinct from `version`. See the version planes note below.</ResponseField>
    <ResponseField name="last_edited_by" type="string (uuid) | null">Most recent editor.</ResponseField>
    <ResponseField name="last_edited_at" type="string (timestamp) | null">Most recent edit time.</ResponseField>
    <ResponseField name="live_deployment_id" type="string (uuid) | null">The currently live deployment snapshot, if any.</ResponseField>
  </Expandable>
</ResponseField>

### The `workflow_schema` object

`workflow_schema` holds the complete workflow definition. The `name`, `description`,
`version`, and `tags` fields above mirror values inside `workflow_schema.metadata`; the
schema is the source of truth.

<ResponseField name="WorkflowDefinition" type="object">
  <Expandable title="fields">
    <ResponseField name="metadata" type="object">Name, description, version, tags, and other workflow-level metadata.</ResponseField>
    <ResponseField name="config" type="object">Workflow-level run configuration.</ResponseField>
    <ResponseField name="state_schema" type="object">The dynamic run-state shape. One field is auto-added per node id (type `Any`) for streaming, alongside any user-defined state fields and loop fields. Each field carries a reducer: `none` (replace), `add` (append/merge arrays), or `update` (dict-merge).</ResponseField>
    <ResponseField name="nodes" type="array" required>The graph steps. Each node has an `id`, a `type` (one of the nine node types), and type-specific config. **Every node writes its result into run state under its own `id`.**</ResponseField>
    <ResponseField name="edges" type="array" required>Connections between nodes. The virtual nodes `__start__` and `__end__` **must not** appear in `nodes[]` — they exist only as edge endpoints.</ResponseField>
    <ResponseField name="entry_point" type="string">The first node. Default `"__start__"`.</ResponseField>
    <ResponseField name="input_parameters" type="array">Optional declared run inputs.</ResponseField>
    <ResponseField name="start_position" type="object">The `{x, y}` canvas position of the `__start__` node.</ResponseField>
  </Expandable>
</ResponseField>

The nine node types are `llm`, `tool`, `agent`, `function`, `conditional`, `interrupt`,
`transformer`, `guardrails`, and `knowledge`. Nodes reference earlier results with the
template syntax `{{nodeId.path}}`. See `/workflow-builder/nodes/overview` and
`/workflow-builder/variables-and-references`.

<Warning>
  **`workflow_schema` round-trips a fixed set of fields.** When the realtime co-editing
  server saves, it keeps a fixed set of top-level fields — `metadata`, `config`,
  `state_schema`, `nodes`, `edges`, `entry_point`, `input_parameters`, `end_points`,
  `start_position` — and does not persist top-level keys outside that set. Do not assume an
  arbitrary key you add at the top level of `workflow_schema` will survive a reload. See
  `/workflow-builder/realtime-coediting`.
</Warning>

### Edit history and the two version planes

Every edit is recorded, and there are two different counters both called "version".

<ResponseField name="edit_history" type="object">
  One append-only record per accepted flush of edits.

  <Expandable title="key fields">
    <ResponseField name="id" type="string (uuid)" required>The history record id.</ResponseField>
    <ResponseField name="workflow_id" type="string (uuid)" required>The workflow.</ResponseField>
    <ResponseField name="edit_version" type="integer" required>The `edit_version` this flush produced — unique per workflow; a duplicate flush is a no-op.</ResponseField>
    <ResponseField name="user_id" type="string (uuid)" required>The editor.</ResponseField>
    <ResponseField name="patches" type="array" required>The original client RFC-6902 JSON Patch operations for this flush.</ResponseField>
    <ResponseField name="created_at" type="string (timestamp)" required>When the flush was recorded.</ResponseField>
  </Expandable>
</ResponseField>

<Note>
  **Two planes, both called "version".** During live editing the realtime room keeps an
  in-memory counter that increments **once per accepted client operation**. The persisted
  `edit_version` increments **once per flush**, and a single flush carries many operations.
  So after N operations and one flush, the in-memory counter has advanced by N while the
  persisted `edit_version` has advanced by 1. Clients see the in-memory plane through live
  acknowledgements and the persisted plane through the saved record — do not copy one over
  the other. See `/workflow-builder/versioning-history` and `/realtime/presence-locks`.
</Note>

<ResponseField name="deployment" type="object">
  An immutable snapshot of a workflow taken at deploy time. A run can be tied to the
  deployment it was launched from.

  <Expandable title="key fields">
    <ResponseField name="id" type="string (uuid)" required>The deployment id.</ResponseField>
    <ResponseField name="workflow_id" type="string (uuid)" required>The source workflow.</ResponseField>
    <ResponseField name="workflow_schema" type="object" required>The frozen workflow definition at deploy time.</ResponseField>
    <ResponseField name="version" type="string">The version label captured at deploy.</ResponseField>
    <ResponseField name="deployment_note" type="string | null">Optional note.</ResponseField>
    <ResponseField name="deployed_by" type="string (uuid) | null">Deploying user.</ResponseField>
    <ResponseField name="created_at" type="string (timestamp)" required>Deploy time.</ResponseField>
  </Expandable>
</ResponseField>

## Runs

<ResponseField name="run" type="object">
  The durable system-of-record for **every** real run. This is the object you query for run
  history and status.

  <Expandable title="key fields">
    <ResponseField name="id" type="string (uuid)" required>The run record's id (UUID).</ResponseField>
    <ResponseField name="run_id" type="string" required>The **durable, unique** run identifier — the stable id to reference a run by. See the identity section below.</ResponseField>
    <ResponseField name="organization_id" type="string (uuid)" required>Owning org.</ResponseField>
    <ResponseField name="workflow_id" type="string (uuid) | null">Source workflow; nulled if the workflow is deleted.</ResponseField>
    <ResponseField name="trigger_type" type="string">How the run was started: `manual`, `api_key`, `scheduled`, or `composer`. Default `manual`.</ResponseField>
    <ResponseField name="user_id" type="string (uuid) | null">Triggering user, where applicable.</ResponseField>
    <ResponseField name="deployment_id" type="string (uuid) | null">The deployment snapshot this run used, if launched from a deployment.</ResponseField>
    <ResponseField name="thread_id" type="string | null">The conversation/checkpoint thread for this run.</ResponseField>
    <ResponseField name="status" type="string" required>One of `pending`, `running`, `succeeded`, `failed`, `cancelled`, `interrupted`, `skipped`. Default `pending`.</ResponseField>
    <ResponseField name="error_message" type="string | null">Failure detail, if any.</ResponseField>
    <ResponseField name="started_at" type="string (timestamp) | null">Run start.</ResponseField>
    <ResponseField name="completed_at" type="string (timestamp) | null">Run completion.</ResponseField>
    <ResponseField name="duration_seconds" type="number | null">Wall-clock duration.</ResponseField>
    <ResponseField name="input_snapshot" type="object | null">The input the run was launched with.</ResponseField>
    <ResponseField name="output_summary" type="object | null">A summary of the run output.</ResponseField>
  </Expandable>
</ResponseField>

Schedules add two more objects. A schedule defines a recurring trigger (`schedule_type` is
`interval` or `cron`), and each fire produces a schedule-run record carrying its own `id`,
`run_id`, `status`, and timing. See `/workflow-builder/execution/schedule` and
`/guides/schedule-a-workflow`.

### Identifiers and the three run-id identities

The single most important identity hazard in ModuleX: **the word "run id" refers to
different things at different layers.** A reader must not assume one identity.

<Tabs>
  <Tab title="The three identities">
    <ResponseField name="per-execution run_id" type="string">
      The identifier used for SSE streaming and run status of one execution. For agentic
      surfaces (Composer and Assistant), a **new** `run_id` is minted on **every resume** — the
      chat keeps the same `thread_id`, but each turn or resume has its own `run_id`. So this
      `run_id` is **not** stable across a conversation.
    </ResponseField>

    <ResponseField name="thread_id (== chat id)" type="string">
      The conversation/checkpoint thread. For chat-backed agents, `thread_id` equals the chat's
      id and is stable across the whole conversation.
    </ResponseField>

    <ResponseField name="durable run_id" type="string">
      The durable run identifier — the unique `run_id` on the run record. A resume **reuses**
      the same `run_id` (the record is updated in place). This is the stable, unique identifier
      the docs reference when they say "a run".
    </ResponseField>
  </Tab>

  <Tab title="The SDK-return hazard">
    Because the SDKs expose runs through several resources, three different SDK return values
    can all be called "the run id". Keep them straight:

    | SDK return                                | What it is                             |
    | ----------------------------------------- | -------------------------------------- |
    | Execution `run_id` (execution methods)    | The per-execution streaming/status id. |
    | Run record `id` (run-record lookup)       | The run record's UUID id.              |
    | Scheduled-run `id` (schedule-run methods) | The schedule-run record's id.          |

    When you persist a reference to a run, persist the durable `run_id`, not a per-execution
    id, and not a record id from another resource. See `/sdks/parity`.
  </Tab>
</Tabs>

<Warning>
  **`Idempotency-Key` does not de-duplicate runs.** The SDKs send the header, but ModuleX
  assigns its own `run_id` per execution and does not use the header for run dedup. Sending
  the same `Idempotency-Key` twice will start two runs. See `/sdks/errors-retries`.
</Warning>

## Credentials

A **credential** is a stored, encrypted authentication record linking an organization to
an integration. See `/concepts/credentials-oauth`,
`/integrations/authentication`, and `/integrations/managing-credentials`.

<ResponseField name="credential" type="object">
  <Expandable title="key fields">
    <ResponseField name="credential_id" type="string (uuid)" required>The credential's unique id. Use it in the API, the SDKs, and all references.</ResponseField>
    <ResponseField name="organization_id" type="string (uuid)" required>Owning org.</ResponseField>
    <ResponseField name="integration_name" type="string" required>The integration this credential is for.</ResponseField>
    <ResponseField name="integration_type" type="string" required>One of `tool`, `llm_provider`, `knowledge_provider`.</ResponseField>
    <ResponseField name="display_name" type="string" required>A human label.</ResponseField>
    <ResponseField name="auth_type" type="string" required>The auth variant. See the note below on the allowed set.</ResponseField>
    <ResponseField name="credentials_metadata" type="object | null">Discovered tool schemas and similar metadata.</ResponseField>
    <ResponseField name="created_by" type="string (uuid) | null">Creating user. A user cannot be deleted while they still own credentials.</ResponseField>
    <ResponseField name="is_valid" type="boolean">Whether the credential currently authenticates.</ResponseField>
    <ResponseField name="is_default" type="boolean">Whether this is the default credential for its integration in the org.</ResponseField>
    <ResponseField name="expires_at" type="string (timestamp) | null">Expiry, where the auth type has one.</ResponseField>
  </Expandable>
</ResponseField>

The credential's secret material is **not** returned by the API — it is stored encrypted
at rest and only decrypted server-side at execution. See `/security/data-encryption` for
how that protection works.

<Note>
  The `auth_type` value you receive is one of six variants — `oauth2`, `bearer_token`,
  `api_key`, `modulex_key`, `custom`, `internal`. Author new credentials against this set.
  See `/integrations/building/manifest-schema`.
</Note>

### Managed-key credentials are indirected

A `modulex_key` credential does **not** expose a provider API key. You reference the
credential, and ModuleX resolves the underlying managed key on your behalf, runs it, and
meters the usage. This is how ModuleX-managed (non-BYOK) usage is billed in credits. See
`/billing/credits`.

## Knowledge

A **knowledge base** is the unit retrieval-augmented generation searches over. It holds
documents, which are split into chunks, which carry vector embeddings. See
`/concepts/knowledge-rag`, `/platform/knowledge/overview`, and
`/platform/knowledge/managed`.

<ResponseField name="knowledge_base" type="object">
  <Expandable title="key fields">
    <ResponseField name="id" type="string (uuid)" required>The knowledge base id.</ResponseField>
    <ResponseField name="organization_id" type="string (uuid)" required>Owning org.</ResponseField>
    <ResponseField name="credential_id" type="string (uuid) | null">For a **native (managed) KB**, the credential backing it. A native KB is one whose embedding provider is the managed provider; its ingest and retrieval are billed in credits. BYOK knowledge bases are not credited.</ResponseField>
    <ResponseField name="name" type="string" required>Display name.</ResponseField>
    <ResponseField name="embedding_config" type="object" required>Per-KB embedding settings (provider, model, dimension).</ResponseField>
    <ResponseField name="chunking_config" type="object" required>Per-KB chunking settings: `strategy` (`recursive`, `token`, or `simple`), `chunk_size`, `chunk_overlap`, `separators`.</ResponseField>
    <ResponseField name="status" type="string">One of `active`, `processing`, `error`, `archived`. Default `active`.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="document" type="object">
  An uploaded file in a knowledge base.

  <Expandable title="key fields">
    <ResponseField name="id" type="string (uuid)" required>The document id.</ResponseField>
    <ResponseField name="knowledge_base_id" type="string (uuid)" required>Parent KB.</ResponseField>
    <ResponseField name="filename" type="string" required>Original file name.</ResponseField>
    <ResponseField name="file_type" type="string" required>One of `pdf`, `docx`, `doc`, `txt`, `md`, `html`, `csv`, `json`, `xlsx`, `pptx`.</ResponseField>
    <ResponseField name="file_size_bytes" type="integer | null">File size.</ResponseField>
    <ResponseField name="file_hash" type="string | null">A content fingerprint used for de-duplication.</ResponseField>
    <ResponseField name="status" type="string">One of `pending`, `processing`, `completed`, `failed`. Default `pending`.</ResponseField>
    <ResponseField name="chunk_count" type="integer">Number of chunks produced. Default `0`.</ResponseField>
    <ResponseField name="token_count" type="integer">Total tokens. Default `0`.</ResponseField>
    <ResponseField name="doc_metadata" type="object">Free-form document metadata.</ResponseField>
    <ResponseField name="uploaded_by_user_id" type="string (uuid)" required>Uploader. A user cannot be deleted while they still have uploaded documents.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="chunk" type="object">
  A text segment of a document with its vector embedding.

  <Expandable title="key fields">
    <ResponseField name="id" type="string (uuid)" required>The chunk id.</ResponseField>
    <ResponseField name="knowledge_base_id" type="string (uuid)" required>Parent KB.</ResponseField>
    <ResponseField name="document_id" type="string (uuid)" required>Parent document.</ResponseField>
    <ResponseField name="content" type="string" required>The chunk text.</ResponseField>
    <ResponseField name="embedding" type="vector | null">The vector embedding for the chunk.</ResponseField>
    <ResponseField name="chunk_index" type="integer" required>Position within the document.</ResponseField>
    <ResponseField name="chunk_metadata" type="object">Free-form chunk metadata.</ResponseField>
  </Expandable>
</ResponseField>

<Warning>
  The advertised "50MB" knowledge upload limit is **not** the enforced cap. The real upload
  cap is a **plan entitlement** that varies by plan. See `/platform/knowledge/documents`
  and `/billing/usage-gating`.
</Warning>

## Billing, credits, and the wallet

Managed usage is priced in **credits** (100 credits = $1.00; 1 credit = $0.01). Each
organization gets a monthly credit allowance from its plan; when the allowance is
exhausted, a paid org with overage enabled spends down a prepaid wallet. See
`/billing/overview`, `/billing/credits`, and `/billing/wallet`.

<ResponseField name="wallet" type="object">
  The prepaid balance an org spends after its plan allowance is gone. There is exactly one
  wallet per organization.

  <Expandable title="key fields">
    <ResponseField name="organization_id" type="string (uuid)" required>The organization this wallet belongs to. One wallet per organization.</ResponseField>
    <ResponseField name="balance" type="number" required>The current balance. **May go negative.**</ResponseField>
    <ResponseField name="extra_usage_enabled" type="boolean">Whether the org may spend beyond its plan allowance (overage). Paid plans only.</ResponseField>
    <ResponseField name="auto_topup_threshold" type="number | null">Balance at which auto top-up fires.</ResponseField>
    <ResponseField name="auto_topup_amount" type="number | null">Auto top-up amount.</ResponseField>
    <ResponseField name="auto_topup_disabled" type="boolean">Set true after repeated auto top-up failures.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="ledger_entry" type="object">
  An append-only entry recording one wallet movement.

  <Expandable title="key fields">
    <ResponseField name="id" type="string (uuid)" required>The ledger entry id.</ResponseField>
    <ResponseField name="organization_id" type="string (uuid)" required>The wallet's org.</ResponseField>
    <ResponseField name="amount" type="number" required>The signed movement (positive for credit, negative for debit).</ResponseField>
    <ResponseField name="kind" type="string" required>One of `topup`, `auto_topup`, `debit`, `refund`, `adjustment`.</ResponseField>
    <ResponseField name="balance_after" type="number | null">The balance after applying this entry.</ResponseField>
    <ResponseField name="stripe_payment_intent_id" type="string | null">For Stripe-funded movements. De-duplicated so a retried payment cannot double-credit.</ResponseField>
    <ResponseField name="extra_metadata" type="object | null">Free-form metadata.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="credit_usage" type="object">
  An append-only record of one managed-usage charge in credits.

  <Expandable title="key fields">
    <ResponseField name="id" type="string (uuid)" required>The usage record id.</ResponseField>
    <ResponseField name="organization_id" type="string (uuid)" required>Charged org.</ResponseField>
    <ResponseField name="credit_cost" type="number" required>The credits charged.</ResponseField>
    <ResponseField name="usage_type" type="string | null">One of `run`, `llm`, `tool`, `file_ingest`, `embedding`, `retrieval`.</ResponseField>
    <ResponseField name="month_offset" type="integer | null">The billing-bucket integer that partitions paid usage per period.</ResponseField>
    <ResponseField name="idempotency_key" type="string | null">The de-duplication key that prevents a double-charge on retry.</ResponseField>
    <ResponseField name="pricing_breakdown" type="object | null">Per-record pricing audit detail.</ResponseField>
    <ResponseField name="composer_chat_id" type="string | null">The Composer chat this charge is attributed to, where applicable.</ResponseField>
  </Expandable>
</ResponseField>

The plan catalog, the active subscription (with its `subscription_status` and an
`operations_suspended` flag), and Stripe billing records round out the billing model. The
`subscriptions` resource is **Python-SDK-only** — there is no JavaScript equivalent. See
`/billing/subscription-lifecycle` and `/sdks/parity`.

## What happens when you delete an object

Deletion behavior is the part that surprises people, so it is worth stating explicitly.

<CardGroup cols={3}>
  <Card title="Deleting an organization" icon="trash">
    Removes all of its data — members, credentials, workflows, runs, chats, knowledge bases,
    and the wallet with its ledger.
  </Card>

  <Card title="Deleting a user" icon="link-slash">
    History is preserved. A run, workflow, or chat the user created keeps existing, with the
    creator link cleared.
  </Card>

  <Card title="Blocked deletes" icon="ban">
    A user who still owns credentials or uploaded documents cannot be deleted until those are
    removed first.
  </Card>
</CardGroup>

## Append-only and system-managed objects

Some records are **append-only history** and never change in place — wallet ledger
entries, credit-usage records, workflow edit history, and run records. Treat them as
immutable history.

Others are **system-managed** and should not be treated as durable user data: the
integration catalog and the node-type builder metadata are regenerated on each deploy. By
contrast, runs, the wallet ledger, and credit-usage records are durable systems-of-record
and are never reset.

## Things to get right

<Steps>
  <Step title="Reference credentials by credential_id">
    A credential is referenced everywhere — including from a native knowledge base — by its
    `credential_id`.
  </Step>

  <Step title="Persist the durable run_id">
    Of the three run identities, only the durable `run_id` is stable and unique across a run's
    lifecycle. Per-execution run ids change on every resume.
  </Step>

  <Step title="Treat owner and admin as the only roles">
    The `member` role is retired. Anything that requires a writer requires `owner` or `admin`.
  </Step>

  <Step title="Expect snake_case on the wire and prefixed metadata fields">
    Raw API responses are snake\_case; free-form metadata surfaces under names like
    `doc_metadata`, `chunk_metadata`, and `extra_metadata`.
  </Step>

  <Step title="Do not rely on Idempotency-Key to de-dup runs">
    The header is accepted but ignored for run dedup; ModuleX assigns its own `run_id`.
  </Step>
</Steps>

## Related pages

<CardGroup cols={2}>
  <Card title="Workflows & runs" icon="diagram-project" href="/concepts/workflows-and-runs">
    The conceptual model behind the workflow and run objects, including the run lifecycle.
  </Card>

  <Card title="Workflow engine & nodes" icon="gears" href="/concepts/workflow-engine">
    How the `workflow_schema`, nodes, edges, and run state execute.
  </Card>

  <Card title="Credentials & OAuth2" icon="key" href="/concepts/credentials-oauth">
    How credentials are stored, resolved, and encrypted.
  </Card>

  <Card title="Errors & status codes" icon="triangle-exclamation" href="/api-reference/errors">
    The error-envelope shapes you get back when an operation is denied or fails.
  </Card>
</CardGroup>
